[Zope-Checkins] CVS: Zope/lib/python/ZServer - DebugLogger.py:1.1.2.1 FCGIServer.py:1.1.2.1 FTPRequest.py:1.1.2.1 FTPResponse.py:1.1.2.1 FTPServer.py:1.1.2.1 HTTPResponse.py:1.1.2.1 HTTPServer.py:1.1.2.1 ICPServer.py:1.1.2.1 INSTALL.txt:1.1.2.1 PCGIServer.py:1.1.2.1 Producers.py:1.1.2.1 README.txt:1.1.2.1 WebDAVSrcHandler.py:1.1.2.1 ZService.py:1.1.2.1 __init__.py:1.1.2.1
Chris McDonough
chrism@zope.com
Tue, 17 Sep 2002 01:16:07 -0400
Update of /cvs-repository/Zope/lib/python/ZServer
In directory cvs.zope.org:/tmp/cvs-serv12650/lib/python/ZServer
Added Files:
Tag: chrism-install-branch
DebugLogger.py FCGIServer.py FTPRequest.py FTPResponse.py
FTPServer.py HTTPResponse.py HTTPServer.py ICPServer.py
INSTALL.txt PCGIServer.py Producers.py README.txt
WebDAVSrcHandler.py ZService.py __init__.py
Log Message:
Moved ZServer into lib/python.
=== Added File Zope/lib/python/ZServer/DebugLogger.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import time, thread
class DebugLogger:
"""
Logs debugging information about how ZServer is handling requests
and responses. This log can be used to help locate troublesome requests.
The format is:
<code> <request id> <time> <data>
where:
'code' is B for begin, I for received input, A for received output,
E for sent output.
'request id' is a unique request id.
'time' is the time in localtime ISO format.
'data' is the HTTP method and the PATH INFO for B, the size of the input
for I, the HTTP status code and the size of the output for A, or
nothing for E.
Note: This facility will be probably be adapted to the zLOG framework.
"""
def __init__(self, filename):
self.filename = filename
self.file=open(filename, 'a+b')
l=thread.allocate_lock()
self._acquire=l.acquire
self._release=l.release
self.log('U', '000000000', 'System startup')
def reopen(self):
self.file.close()
self.file=open(self.filename, 'a+b')
self.log('U', '000000000', 'Logfile reopened')
def log(self, code, request_id, data=''):
self._acquire()
try:
t=time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(time.time()))
self.file.write(
'%s %s %s %s\n' % (code, request_id, t, data)
)
self.file.flush()
finally:
self._release()
def log(*args): pass
=== Added File Zope/lib/python/ZServer/FCGIServer.py === (666/766 lines abridged)
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer/Medusa FastCGI server, by Robin Dunn.
Accepts connections from a FastCGI enabled webserver, receives request
info using the FastCGi protocol, and then hands the request off to
ZPublisher for processing. The response is then handed back to the
webserver to send down to the browser.
See http://www.fastcgi.com/fcgi-devkit-2.1/doc/fcgi-spec.html for the
protocol specificaition.
"""
__version__ = "1.0"
#----------------------------------------------------------------------
import asynchat, asyncore
from medusa import logger
from medusa.counter import counter
from medusa.http_server import compute_timezone_for_log
from ZServer import CONNECTION_LIMIT, requestCloseOnExec
from PubCore import handle
from PubCore.ZEvent import Wakeup
from ZPublisher.HTTPResponse import HTTPResponse
from ZPublisher.HTTPRequest import HTTPRequest
from Producers import ShutdownProducer, LoggingProducer, file_part_producer, file_close_producer
import DebugLogger
from cStringIO import StringIO
from tempfile import TemporaryFile
import socket, string, os, sys, time
from types import StringType
import thread
[-=- -=- -=- 666 lines omitted -=- -=- -=-]
if t is not None:
self.stdout.write((file_close_producer(t), 0))
self._tempfile=None
self.channel.sendStreamTerminator(FCGI_STDOUT)
self.channel.sendEndRecord()
self.stdout.close()
self.stderr.close()
if not self.channel.closed:
self.channel.push_with_producer(LoggingProducer(self.channel,
self.stdout.length,
'log_request'), 0)
if self._shutdownRequested():
self.channel.push(ShutdownProducer(), 0)
Wakeup(lambda: asyncore.close_all())
else:
self.channel.push(None,0)
Wakeup()
self.channel=None
#----------------------------------------------------------------------
class FCGIPipe:
"""
This class acts like a file and is used to catch stdout/stderr
from ZPublisher and create FCGI records out of the data stream to
send back to the web server.
"""
def __init__(self, channel, recType):
self.channel = channel
self.recType = recType
self.length = 0
def write(self, data):
if type(data)==type(''):
datalen = len(data)
else:
p, datalen = data
if data:
self.channel.sendDataRecord(data, self.recType)
self.length = self.length + datalen
def close(self):
self.channel = None
#----------------------------------------------------------------------
=== Added File Zope/lib/python/ZServer/FTPRequest.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
FTP Request class for FTP server.
The FTP Request does the dirty work of turning an FTP request into something
that ZPublisher can understand.
"""
from ZPublisher.HTTPRequest import HTTPRequest
from cStringIO import StringIO
import os
from base64 import encodestring
import re
class FTPRequest(HTTPRequest):
def __init__(self, path, command, channel, response, stdin=None,
environ=None,globbing=None,recursive=0):
# we need to store the globbing information to pass it
# to the ZPublisher and the manage_FTPlist function
# (ajung)
self.globbing = globbing
self.recursive= recursive
if stdin is None: stdin=StringIO()
if environ is None:
environ=self._get_env(path, command, channel, stdin)
self._orig_env=environ
HTTPRequest.__init__(self, stdin, environ, response, clean=1)
# support for cookies and cookie authentication
self.cookies=channel.cookies
if not self.cookies.has_key('__ac') and channel.userid != 'anonymous':
self.other['__ac_name']=channel.userid
self.other['__ac_password']=channel.password
for k,v in self.cookies.items():
if not self.other.has_key(k):
self.other[k]=v
def retry(self):
self.retry_count=self.retry_count+1
r=self.__class__(stdin=self.stdin,
environ=self._orig_env,
response=self.response.retry(),
channel=self, # For my cookies
)
return r
def _get_env(self, path, command, channel, stdin):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % channel.module
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=channel.server.SERVER_IDENT
if channel.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % re.sub('\012','',
encodestring('%s:%s' % (channel.userid, channel.password)))
env['SERVER_NAME']=channel.server.hostname
env['SERVER_PORT']=str(channel.server.port)
env['REMOTE_ADDR']=channel.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# FTP commands
#
if type(command)==type(()):
args=command[1:]
command=command[0]
if command in ('LST','CWD','PASS'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPlist')
elif command in ('MDTM','SIZE'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPstat')
elif command=='RETR':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPget')
elif command in ('RMD','DELE'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_delObjects')
env['QUERY_STRING']='ids=%s' % args[0]
elif command=='MKD':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_addFolder')
env['QUERY_STRING']='id=%s' % args[0]
elif command=='RNTO':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_renameObject')
env['QUERY_STRING']='id=%s&new_id=%s' % (args[0],args[1])
elif command=='STOR':
env['PATH_INFO']=self._join_paths(channel.path, path)
env['REQUEST_METHOD']='PUT'
env['CONTENT_LENGTH']=len(stdin.getvalue())
else:
env['PATH_INFO']=self._join_paths(channel.path, path, command)
# Fake in globbing information
env['GLOBBING'] = self.globbing
env['FTP_RECURSIVE'] = self.recursive
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=path.replace(os.sep,'/')
return path
=== Added File Zope/lib/python/ZServer/FTPResponse.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Response class for the FTP Server.
"""
from ZServer.HTTPResponse import ZServerHTTPResponse
from PubCore.ZEvent import Wakeup
from cStringIO import StringIO
import marshal
class FTPResponse(ZServerHTTPResponse):
"""
Response to an FTP command
"""
def __str__(self):
# return ZServerHTTPResponse.__str__(self)
# ZServerHTTPResponse.__str__(self) return HTTP headers
# Why should be send them to the FTP client ??? (ajung)
return ''
def outputBody(self):
pass
def setCookie(self, name, value, **kw):
self.cookies[name]=value
def appendCookie(self, name, value):
self.cookies[name]=self.cookies[name] + value
def expireCookie(self, name, **kw):
if self.cookies.has_key(name):
del self.cookies[name]
def _cookie_list(self):
return []
def _marshalledBody(self):
return marshal.loads(self.body)
def setMessage(self, message):
self._message = message
def getMessage(self):
return getattr(self, '_message', '')
class CallbackPipe:
"""
Sends response object to a callback. Doesn't write anything.
The callback takes place in Medusa's thread, not the request thread.
"""
def __init__(self, callback, args):
self._callback=callback
self._args=args
self._producers=[]
def close(self):
pass
def write(self, text, l=None):
if text:
self._producers.append(text)
def finish(self, response):
self._response=response
Wakeup(self.apply) # move callback to medusas thread
def apply(self):
result=apply(self._callback, self._args+(self._response,))
# break cycles
self._callback=None
self._response=None
self._args=None
return result
def make_response(channel, callback, *args):
# XXX should this be the FTPResponse constructor instead?
r=FTPResponse(stdout=CallbackPipe(callback, args), stderr=StringIO())
r.setHeader('content-type','text/plain')
r.cookies=channel.cookies
return r
=== Added File Zope/lib/python/ZServer/FTPServer.py === (537/637 lines abridged)
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""ZServer FTP Channel for use the medusa's ftp server.
FTP Service for Zope.
This server allows FTP connections to Zope. In general FTP is used
to manage content. You can:
* Create and delete Folders, Documents, Files, and Images
* Edit the contents of Documents, Files, Images
In the future, FTP may be used to edit object properties.
FTP Protocol
The FTP protocol for Zope gives Zope objects a way to make themselves
available to FTP services. See the 'lib/python/OFS/FTPInterface.py' for
more details.
FTP Permissions
FTP access is controlled by one permission: 'FTP access' if bound to a
role, users of that role will be able to list directories, and cd to
them. Creating and deleting and changing objects are all governed by
existing Zope permissions.
Permissions are to a certain extent reflected in the permission bits
listed in FTP file listings.
FTP Authorization
Zope supports both normal and anonymous logins. It can be difficult
to authorize Zope users since they are defined in distributed user
databases. Normally, all logins will be accepted and then the user must
proceed to 'cd' to a directory in which they are authorized. In this
case for the purpose of FTP limits, the user is considered anonymous
until they cd to an authorized directory.
[-=- -=- -=- 537 lines omitted -=- -=- -=-]
return None
if total <= self.total_limit:
return 1
class FTPServer(ftp_server):
"""FTP server for Zope."""
ftp_channel_class = zope_ftp_channel
limiter=FTPLimiter(10,1)
shutup=0
def __init__(self,module,*args,**kw):
self.shutup=1
apply(ftp_server.__init__, (self, None) + args, kw)
self.shutup=0
self.module=module
self.log_info('FTP server started at %s\n'
'\tHostname: %s\n\tPort: %d' % (
time.ctime(time.time()),
self.hostname,
self.port
))
def log_info(self, message, type='info'):
if self.shutup: return
asyncore.dispatcher.log_info(self, message, type)
def create_socket(self, family, type):
asyncore.dispatcher.create_socket(self, family, type)
requestCloseOnExec(self.socket)
def handle_accept (self):
try:
conn, addr = self.accept()
except TypeError:
# unpack non-sequence as result of accept
# returning None (in case of EWOULDBLOCK)
return
self.total_sessions.increment()
self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1]))
self.ftp_channel_class (self, conn, addr, self.module)
def readable(self):
return len(asyncore.socket_map) < CONNECTION_LIMIT
def listen(self, num):
# override asyncore limits for nt's listen queue size
self.accepting = 1
return self.socket.listen (num)
=== Added File Zope/lib/python/ZServer/HTTPResponse.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer HTTPResponse
The HTTPResponse class takes care of server headers, response munging
and logging duties.
"""
import time, re, sys, tempfile
from cStringIO import StringIO
import thread
from ZPublisher.HTTPResponse import HTTPResponse
from medusa.http_date import build_http_date
from PubCore.ZEvent import Wakeup
from medusa.producers import hooked_producer
from medusa import http_server
import asyncore
from Producers import ShutdownProducer, LoggingProducer, CallbackProducer, \
file_part_producer, file_close_producer
from types import LongType
import DebugLogger
class ZServerHTTPResponse(HTTPResponse):
"Used to push data into a channel's producer fifo"
# Set this value to 1 if streaming output in
# HTTP/1.1 should use chunked encoding
http_chunk=1
http_chunk_size=1024
# defaults
_http_version='1.0'
_http_connection='close'
_server_version='Zope/2.0 ZServer/2.0'
# using streaming response
_streaming=0
# using chunking transfer-encoding
_chunking=0
def __str__(self,
html_search=re.compile('<html>',re.I).search,
):
if self._wrote:
if self._chunking:
return '0\r\n\r\n'
else:
return ''
headers=self.headers
body=self.body
# set 204 (no content) status if 200 and response is empty
# and not streaming
if not headers.has_key('content-type') and \
not headers.has_key('content-length') and \
not self._streaming and \
self.status == 200:
self.setStatus('nocontent')
# add content length if not streaming
if not headers.has_key('content-length') and \
not self._streaming:
self.setHeader('content-length',len(body))
content_length= headers.get('content-length', None)
if content_length>0 :
self.setHeader('content-length', content_length)
headersl=[]
append=headersl.append
status=headers.get('status', '200 OK')
# status header must come first.
append("HTTP/%s %s" % (self._http_version or '1.0' , status))
if headers.has_key('status'):
del headers['status']
if not headers.has_key("Etag"):
self.setHeader('Etag','')
# add zserver headers
append('Server: %s' % self._server_version)
append('Date: %s' % build_http_date(time.time()))
if self._http_version=='1.0':
if self._http_connection=='keep-alive' and \
self.headers.has_key('content-length'):
self.setHeader('Connection','Keep-Alive')
else:
self.setHeader('Connection','close')
# Close the connection if we have been asked to.
# Use chunking if streaming output.
if self._http_version=='1.1':
if self._http_connection=='close':
self.setHeader('Connection','close')
elif not self.headers.has_key('content-length'):
if self.http_chunk and self._streaming:
self.setHeader('Transfer-Encoding','chunked')
self._chunking=1
else:
self.setHeader('Connection','close')
for key, val in headers.items():
if key.lower()==key:
# only change non-literal header names
key="%s%s" % (key[:1].upper(), key[1:])
start=0
l=key.find('-',start)
while l >= start:
key="%s-%s%s" % (key[:l],key[l+1:l+2].upper(),key[l+2:])
start=l+1
l=key.find('-',start)
append("%s: %s" % (key, val))
if self.cookies:
headersl=headersl+self._cookie_list()
headersl[len(headersl):]=[self.accumulated_headers, body]
return "\r\n".join(headersl)
_tempfile=None
_templock=None
_tempstart=0
def write(self,data):
"""\
Return data as a stream
HTML data may be returned using a stream-oriented interface.
This allows the browser to display partial results while
computation of a response to proceed.
The published object should first set any output headers or
cookies on the response object.
Note that published objects must not generate any errors
after beginning stream-oriented output.
"""
stdout=self.stdout
if not self._wrote:
l=self.headers.get('content-length', None)
if l is not None:
try:
if type(l) is type(''): l=int(l)
if l > 128000:
self._tempfile=tempfile.TemporaryFile()
self._templock=thread.allocate_lock()
except: pass
self._streaming=1
stdout.write(str(self))
self._wrote=1
if not data: return
if self._chunking:
data = '%x\r\n%s\r\n' % (len(data),data)
l=len(data)
t=self._tempfile
if t is None or l<200:
stdout.write(data)
else:
b=self._tempstart
e=b+l
self._templock.acquire()
try:
t.seek(b)
t.write(data)
finally:
self._templock.release()
self._tempstart=e
stdout.write(file_part_producer(t,self._templock,b,e), l)
_retried_response = None
def _finish(self):
if self._retried_response:
try:
self._retried_response._finish()
finally:
self._retried_response = None
return
stdout=self.stdout
t=self._tempfile
if t is not None:
stdout.write(file_close_producer(t), 0)
self._tempfile=None
stdout.finish(self)
stdout.close()
self.stdout=None # need to break cycle?
self._request=None
def retry(self):
"""Return a request object to be used in a retry attempt
"""
# This implementation is a bit lame, because it assumes that
# only stdout stderr were passed to the constructor. OTOH, I
# think that that's all that is ever passed.
response=self.__class__(stdout=self.stdout, stderr=self.stderr)
response.headers=self.headers
response._http_version=self._http_version
response._http_connection=self._http_connection
response._server_version=self._server_version
self._retried_response = response
return response
class ChannelPipe:
"""Experimental pipe from ZPublisher to a ZServer Channel.
Should only be used by one thread at a time. Note also that
the channel will be being handled by another thread, thus
restrict access to channel to the push method only."""
def __init__(self, request):
self._channel=request.channel
self._request=request
self._shutdown=0
self._close=0
self._bytes=0
def write(self, text, l=None):
if self._channel.closed:
return
if l is None: l=len(text)
self._bytes=self._bytes + l
self._channel.push(text,0)
Wakeup()
def close(self):
DebugLogger.log('A', id(self._request),
'%s %s' % (self._request.reply_code, self._bytes))
if not self._channel.closed:
self._channel.push(LoggingProducer(self._request, self._bytes), 0)
self._channel.push(CallbackProducer(self._channel.done), 0)
self._channel.push(CallbackProducer(
lambda t=('E', id(self._request)): apply(DebugLogger.log, t)), 0)
if self._shutdown:
self._channel.push(ShutdownProducer(), 0)
Wakeup()
else:
if self._close: self._channel.push(None, 0)
Wakeup()
else:
# channel closed too soon
self._request.log(self._bytes)
DebugLogger.log('E', id(self._request))
if self._shutdown:
Wakeup(lambda: asyncore.close_all())
else:
Wakeup()
self._channel=None #need to break cycles?
self._request=None
def flush(self): pass # yeah, whatever
def finish(self, response):
if response._shutdownRequested():
self._shutdown = 1
if response.headers.get('connection','') == 'close' or \
response.headers.get('Connection','') == 'close':
self._close=1
self._request.reply_code=response.status
is_proxying_match = re.compile(r'[^ ]* [^ \\]*:').match
proxying_connection_re = re.compile ('Proxy-Connection: (.*)', re.IGNORECASE)
def make_response(request, headers):
"Simple http response factory"
# should this be integrated into the HTTPResponse constructor?
response=ZServerHTTPResponse(stdout=ChannelPipe(request), stderr=StringIO())
response._http_version=request.version
if request.version=='1.0' and is_proxying_match(request.request):
# a request that was made as if this zope was an http 1.0 proxy.
# that means we have to use some slightly different http
# headers to manage persistent connections.
connection_re = proxying_connection_re
else:
# a normal http request
connection_re = http_server.CONNECTION
response._http_connection = http_server.get_header(connection_re,
request.header).lower()
response._server_version=request.channel.server.SERVER_IDENT
return response
=== Added File Zope/lib/python/ZServer/HTTPServer.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Medusa HTTP server for Zope
changes from Medusa's http_server
Request Threads -- Requests are processed by threads from a thread
pool.
Output Handling -- Output is pushed directly into the producer
fifo by the request-handling thread. The HTTP server does not do
any post-processing such as chunking.
Pipelineable -- This is needed for protocols such as HTTP/1.1 in
which mutiple requests come in on the same channel, before
responses are sent back. When requests are pipelined, the client
doesn't wait for the response before sending another request. The
server must ensure that responses are sent back in the same order
as requests are received.
"""
import sys
import re
import os
import types
import thread
import time
import socket
from cStringIO import StringIO
from PubCore import handle
from HTTPResponse import make_response
from ZPublisher.HTTPRequest import HTTPRequest
from medusa.http_server import http_server,get_header, http_channel, VERSION_STRING
import asyncore
from medusa import counter, producers
from medusa.test import max_sockets
from medusa.default_handler import unquote
from asyncore import compact_traceback, dispatcher
from ZServer import CONNECTION_LIMIT, ZOPE_VERSION, ZSERVER_VERSION
from ZServer import requestCloseOnExec
from zLOG import LOG, register_subsystem, BLATHER, INFO, WARNING, ERROR
import DebugLogger
from medusa import logger
register_subsystem('ZServer HTTPServer')
CONTENT_LENGTH = re.compile('Content-Length: ([0-9]+)',re.I)
CONNECTION = re.compile('Connection: (.*)', re.I)
USER_AGENT = re.compile('User-Agent: (.*)', re.I)
# maps request some headers to environment variables.
# (those that don't start with 'HTTP_')
header2env={'content-length' : 'CONTENT_LENGTH',
'content-type' : 'CONTENT_TYPE',
'connection' : 'CONNECTION_TYPE',
}
class zhttp_collector:
def __init__(self, handler, request, size):
self.handler = handler
self.request = request
if size > 524288:
# write large upload data to a file
from tempfile import TemporaryFile
self.data = TemporaryFile('w+b')
else:
self.data = StringIO()
request.channel.set_terminator(size)
request.collector=self
# put and post collection methods
#
def collect_incoming_data (self, data):
self.data.write(data)
def found_terminator(self):
# reset collector
self.request.channel.set_terminator('\r\n\r\n')
self.request.collector=None
# finish request
self.data.seek(0)
r=self.request
d=self.data
del self.request
del self.data
self.handler.continue_request(d,r)
class zhttp_handler:
"A medusa style handler for zhttp_server"
_force_connection_close = 0
def __init__ (self, module, uri_base=None, env=None):
"""Creates a zope_handler
module -- string, the name of the module to publish
uri_base -- string, the base uri of the published module
defaults to '/<module name>' if not given.
env -- dictionary, environment variables to be overridden.
Replaces standard variables with supplied ones.
"""
self.module_name=module
self.env_override=env or {}
self.hits = counter.counter()
# if uri_base is unspecified, assume it
# starts with the published module name
#
if uri_base is None:
uri_base='/%s' % module
elif uri_base == '':
uri_base='/'
else:
if uri_base[0] != '/':
uri_base='/'+uri_base
if uri_base[-1] == '/':
uri_base=uri_base[:-1]
self.uri_base=uri_base
uri_regex='%s.*' % self.uri_base
self.uri_regex = re.compile(uri_regex)
def match(self, request):
uri = request.uri
if self.uri_regex.match(uri):
return 1
else:
return 0
def handle_request(self,request):
self.hits.increment()
DebugLogger.log('B', id(request), '%s %s' % (request.command.upper(), request.uri))
size=get_header(CONTENT_LENGTH, request.header)
if size and size != '0':
size=int(size)
zhttp_collector(self, request, size)
else:
sin=StringIO()
self.continue_request(sin,request)
def get_environment(self, request,
# These are strictly performance hackery...
h2ehas=header2env.has_key,
h2eget=header2env.get,
workdir=os.getcwd(),
ospath=os.path,
):
(path, params, query, fragment) = request.split_uri()
if params: path = path + params # undo medusa bug!
while path and path[0] == '/':
path = path[1:]
if '%' in path:
path = unquote(path)
if query:
# ZPublisher doesn't want the leading '?'
query = query[1:]
server=request.channel.server
env = {}
env['REQUEST_METHOD']=request.command.upper()
env['SERVER_PORT']=str(server.port)
env['SERVER_NAME']=server.server_name
env['SERVER_SOFTWARE']=server.SERVER_IDENT
env['SERVER_PROTOCOL']="HTTP/"+request.version
env['channel.creation_time']=request.channel.creation_time
if self.uri_base=='/':
env['SCRIPT_NAME']=''
env['PATH_INFO']='/' + path
else:
env['SCRIPT_NAME'] = self.uri_base
try:
path_info=path.split(self.uri_base[1:],1)[1]
except:
path_info=''
env['PATH_INFO']=path_info
env['PATH_TRANSLATED']=ospath.normpath(ospath.join(
workdir, env['PATH_INFO']))
if query:
env['QUERY_STRING'] = query
env['GATEWAY_INTERFACE']='CGI/1.1'
env['REMOTE_ADDR']=request.channel.addr[0]
# This is a really bad hack to support WebDAV
# clients accessing documents through GET
# on the HTTP port. We check if your WebDAV magic
# machinery is enabled and if the client is recognized
# as WebDAV client. If yes, we fake the environment
# to pretend the ZPublisher to have a WebDAV request.
# This sucks like hell but it works pretty fine ;-)
if env['REQUEST_METHOD']=='GET':
wdav_client_reg = getattr(sys,'WEBDAV_SOURCE_PORT_CLIENTS',None)
if wdav_client_reg:
agent = get_header(USER_AGENT,request.header)
if wdav_client_reg(agent):
env['WEBDAV_SOURCE_PORT'] = 1
path_info = env['PATH_INFO']
path_info = os.path.join(path_info,'manage_FTPget')
path_info = os.path.normpath(path_info)
if os.sep != '/': path_info = path_info.replace(os.sep,'/')
env['PATH_INFO'] = path_info
# If we're using a resolving logger, try to get the
# remote host from the resolver's cache.
if hasattr(server.logger, 'resolver'):
dns_cache=server.logger.resolver.cache
if dns_cache.has_key(env['REMOTE_ADDR']):
remote_host=dns_cache[env['REMOTE_ADDR']][2]
if remote_host is not None:
env['REMOTE_HOST']=remote_host
env_has=env.has_key
for header in request.header:
key,value=header.split(":",1)
key=key.lower()
value=value.strip()
if h2ehas(key) and value:
env[h2eget(key)]=value
else:
key='HTTP_%s' % ("_".join(key.split( "-"))).upper()
if value and not env_has(key):
env[key]=value
env.update(self.env_override)
return env
def continue_request(self, sin, request):
"continue handling request now that we have the stdin"
s=get_header(CONTENT_LENGTH, request.header)
if s:
s=int(s)
else:
s=0
DebugLogger.log('I', id(request), s)
env=self.get_environment(request)
zresponse=make_response(request,env)
if self._force_connection_close:
zresponse._http_connection = 'close'
zrequest=HTTPRequest(sin, env, zresponse)
request.channel.current_request=None
request.channel.queue.append((self.module_name, zrequest, zresponse))
request.channel.work()
def status(self):
return producers.simple_producer("""
<li>Zope Handler
<ul>
<li><b>Published Module:</b> %s
<li><b>Hits:</b> %s
</ul>""" %(self.module_name, self.hits)
)
class zhttp_channel(http_channel):
"http channel"
closed=0
zombie_timeout=100*60 # 100 minutes
def __init__(self, server, conn, addr):
http_channel.__init__(self, server, conn, addr)
requestCloseOnExec(conn)
self.queue=[]
self.working=0
def push(self, producer, send=1):
# this is thread-safe when send is false
# note, that strings are not wrapped in
# producers by default
if self.closed:
return
self.producer_fifo.push(producer)
if send: self.initiate_send()
push_with_producer=push
def work(self):
"try to handle a request"
if not self.working:
if self.queue:
self.working=1
try: module_name, request, response=self.queue.pop(0)
except: return
handle(module_name, request, response)
def close(self):
self.closed=1
while self.queue:
self.queue.pop()
if self.current_request is not None:
self.current_request.channel=None # break circ refs
self.current_request=None
while self.producer_fifo:
p=self.producer_fifo.first()
if p is not None and type(p) != types.StringType:
p.more() # free up resources held by producer
self.producer_fifo.pop()
dispatcher.close(self)
def done(self):
"Called when a publishing request is finished"
self.working=0
self.work()
def kill_zombies(self):
now = int (time.time())
for channel in asyncore.socket_map.values():
if channel.__class__ == self.__class__:
if (now - channel.creation_time) > channel.zombie_timeout:
channel.close()
class zhttp_server(http_server):
"http server"
SERVER_IDENT='Zope/%s ZServer/%s' % (ZOPE_VERSION,ZSERVER_VERSION)
channel_class = zhttp_channel
shutup=0
def __init__ (self, ip, port, resolver=None, logger_object=None):
self.shutup=1
http_server.__init__(self, ip, port, resolver, logger_object)
self.shutup=0
self.log_info('HTTP server started at %s\n'
'\tHostname: %s\n\tPort: %d' % (
time.ctime(time.time()),
self.server_name,
self.server_port
))
def log_info(self, message, type='info'):
if self.shutup: return
dispatcher.log_info(self, message, type)
def create_socket(self, family, type):
dispatcher.create_socket(self, family, type)
requestCloseOnExec(self.socket)
def readable(self):
return self.accepting and \
len(asyncore.socket_map) < CONNECTION_LIMIT
def listen(self, num):
# override asyncore limits for nt's listen queue size
self.accepting = 1
return self.socket.listen (num)
=== Added File Zope/lib/python/ZServer/ICPServer.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
# Medusa ICP server
#
# Why would you want to use this?
# see http://www.zope.org/Members/htrd/icp/intro
import sys, string, os, socket, errno, struct
import asyncore
from medusa import counter
ICP_OP_QUERY = 1
ICP_OP_HIT = 2
ICP_OP_MISS = 3
ICP_OP_ERR = 4
ICP_OP_MISS_NOFETCH = 21
ICP_OP_DENIED = 22
class BaseICPServer(asyncore.dispatcher):
REQUESTS_PER_LOOP = 4
def __init__ (self,ip,port):
asyncore.dispatcher.__init__(self)
self.create_socket (socket.AF_INET, socket.SOCK_DGRAM)
self.set_reuse_addr()
self.bind((ip,port))
if ip=='':
addr = 'any'
else:
addr = ip
self.log_info('ICP server started\n\tAddress: %s\n\tPort: %s' % (addr,port) )
def handle_read(self):
for i in range(self.REQUESTS_PER_LOOP):
try:
request, whence = self.socket.recvfrom(16384)
except socket.error,e:
if e[0]==errno.EWOULDBLOCK:
break
else:
raise
else:
if self.check_whence(whence):
reply = self.calc_reply(request)
if reply:
self.socket.sendto(reply,whence)
def readable(self):
return 1
def writable(self):
return 0
def handle_write (self):
self.log_info ('unexpected write event', 'warning')
def handle_error (self): # don't close the socket on error
(file,fun,line), t, v, tbinfo = asyncore.compact_traceback()
self.log_info('Problem in ICP (%s:%s %s)' % (t, v, tbinfo),
'error')
def check_whence(self,whence):
return 1
def calc_reply(self,request):
if len(request)>20:
opcode,version,length,number,options,opdata,junk = struct.unpack('!BBHIIII',request[:20])
if version==2:
if opcode==ICP_OP_QUERY:
if len(request)!=length:
out_opcode = ICP_OP_ERR
else:
url = request[24:]
if url[-1:]=='\x00':
url = url[:-1]
out_opcode = self.check_url(url)
return struct.pack('!BBHIIII',out_opcode,2,20,number,0,0,0)
def check_url(self,url):
# derived classes replace this with a more
# useful policy
return ICP_OP_MISS
class ICPServer(BaseICPServer):
# Products that want to do special ICP handling should .append their hooks into
# this list. Each hook is called in turn with the URL as a parameter, and
# they must return an ICP_OP code from above or None. The first
# non-None return is used as the ICP response
hooks = []
def check_url(self,url):
for hook in self.hooks:
r = hook(url)
if r is not None:
return r
return ICP_OP_MISS
=== Added File Zope/lib/python/ZServer/INSTALL.txt ===
ZServer Installation
--------------------
Requirements
ZServer comes with Zope 2. Though ZServer can be used with earlier
versions of Zope, this is not supported and not covered by this
document.
Configuration
To run ZServer you simply execute the z2.py start script which is
located in your Zope directory. You can pass commandline arguments
to the start script in order to run Zope with different options. In
order to see a list of options use the '-h' help option.
Here's an example of use::
$ python2.1 z2.py -w 8888 -f "" -p "" -m "" &
This example starts Zope using a web server on port 8888. It does
not start and FTP server, or a PCGI server, or a Monitor server. It
also starts the server running in the backaground.
Shell scripts and batch files
You may also wish to create a shell script (or batch file under
win32) to set environment variables (such as ZOPE_DEBUG_MODE and
PYTHONHOME) and run the start script.
Here's an example shell script for a binary Zope release::
ZOPE_DEBUG_MODE=1
export ZOPE_DEBUG_MODE
PYTHONHOME=/home/Zope
export PYTHONHOME
/home/Zope/bin/python /home/Zope/z2.py -w 9673 &
Note: If ZServer fails because it can't find some standard Python
libaries there's a good bet that you need to set the PYTHONHOME as
shown above.
Here's an example batch file::
set ZOPE_DEBUG_MODE=1
"\Program Files\Zope\bin\python" "\Program Files\Zope\z2.py -w
8888 -f 8021"
Now you're ready to go.
Starting ZServer
To start ZServer run the start script::
$ python2.1 z2.py
To stop the server type 'control-c'.
Note: If you've created a shell script or batch file to run ZServer
use that instead.
You should see some Medusa information come up on the screen as Zope
starts.
A log file will be written to the 'var' directory, named
'Z2.log' by default.
Using ZServer
Once you start ZServer is will publish Zope (or any Python module)
on HTTP and/or FTP. To access Zope via HTTP point your browser at
the server like so::
http://www.example.com:9673/
This assumes that you have chosen to put HTTP on port 9673 and that
you are publishing a module named whose URL prefix is set to ''.
Note: to publish Zope normally you publish the 'lib/python/Zope.py'
module.
To access Zope via FTP you need to FTP to it at the port you set FTP
to run on. For example::
ftp www.example.com 9221
This opens a FTP session to your machine on port 9221, ZServer's
default FTP port. When you are prompted to log in you should supply
a Zope username and password. (Probably you should use an account
with the 'Manager' role, unless you have configured Zope to allow
FTP access to the 'Anonymous' role.) You can also enter 'anonymous'
and any password for anonymous FTP access. Once you have logged in
you can start issuing normal FTP commands.
Right now ZServer supports most basic FTP commands.
Note: When you log in your working directory is set to '/'. If you
do not have FTP permissions in this directory, you will need to 'cd'
to a directory where you have permissions before you can do
anything. See above for more information about logging into FTP.
Advanced Usage: zdaemon.py and the Zope NT service.
One problem you may notice with ZServer is that once the server is
shutdown, either through the web management interface, or by some
other means, it will not automatically be restarted.
On Unix you can use zdeamon.py to keep Zope running. Specifying
the '-Z' switch when starting Zope runs zdaemon.py. Zdeamon
will restart Zope when it Zope is restarted through the web, and in
case of an unexpected error.
On NT, use the Zope service for the same functionality. See ZServer.py
for more information on running ZServer as a service.
Where to go from here
Check out the README.txt file. It contains information on what
ZServer can do, how it works and and what you can do if you run into
problems.
And don't forget to have fun!
=== Added File Zope/lib/python/ZServer/PCGIServer.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Medusa PCGI server.
This server functions as the PCGI publisher--it accepts the request
from the PCGI wrapper CGI program, services the request, and sends
back the response.
It should work with both inet and unix domain sockets.
Why would you want to use it? Using PCGI to connect to ZServer from
another webserver is similar to using the web server as a proxy,
with the difference, that the web server gets to control the
environment and headers completely.
Note that ZServer can operate multiple PCGI servers.
"""
from medusa import logger
import asynchat, asyncore
from medusa.counter import counter
from medusa.http_server import compute_timezone_for_log
from asyncore import compact_traceback
from ZServer import CONNECTION_LIMIT, requestCloseOnExec
from PubCore import handle
from PubCore.ZEvent import Wakeup
from ZPublisher.HTTPResponse import HTTPResponse
from ZPublisher.HTTPRequest import HTTPRequest
from Producers import ShutdownProducer, LoggingProducer, CallbackProducer
import DebugLogger
from cStringIO import StringIO
from tempfile import TemporaryFile
import socket, string, os, sys, time
from types import StringType, TupleType
tz_for_log=compute_timezone_for_log()
class PCGIChannel(asynchat.async_chat):
"""Processes a PCGI request by collecting the env and stdin and
then passing them to ZPublisher. The result is wrapped in a
producer and sent back."""
closed=0
def __init__(self,server,sock,addr):
self.server = server
self.addr = addr
asynchat.async_chat.__init__ (self, sock)
requestCloseOnExec(sock)
self.env={}
self.data=StringIO()
self.set_terminator(10)
self.size=None
self.done=None
def found_terminator(self):
if self.size is None:
# read the next size header
# and prepare to read env or stdin
self.data.seek(0)
self.size=string.atoi(self.data.read())
self.set_terminator(self.size)
if self.size==0:
DebugLogger.log('I', id(self), 0)
self.set_terminator('\r\n')
self.data=StringIO()
self.send_response()
elif self.size > 1048576:
self.data=TemporaryFile('w+b')
else:
self.data=StringIO()
elif not self.env:
# read env
self.size=None
self.data.seek(0)
buff=self.data.read()
for line in string.split(buff,'\000'):
try:
k,v = string.split(line,'=',1)
self.env[k] = v
except:
pass
# Hack around broken IIS PATH_INFO
# maybe, this should go in ZPublisher...
if self.env.has_key('SERVER_SOFTWARE') and \
string.find(self.env['SERVER_SOFTWARE'],
'Microsoft-IIS') != -1:
script = filter(None,string.split(
string.strip(self.env['SCRIPT_NAME']),'/'))
path = filter(None,string.split(
string.strip(self.env['PATH_INFO']),'/'))
self.env['PATH_INFO'] = '/' + string.join(path[len(script):],'/')
self.data=StringIO()
DebugLogger.log('B', id(self),
'%s %s' % (self.env['REQUEST_METHOD'],
self.env.get('PATH_INFO' ,'/')))
# now read the next size header
self.set_terminator(10)
else:
DebugLogger.log('I', id(self), self.terminator)
# we're done, we've got both env and stdin
self.set_terminator('\r\n')
self.data.seek(0)
self.send_response()
def send_response(self):
# create an output pipe by passing request to ZPublisher,
# and requesting a callback of self.log with the module
# name and PATH_INFO as an argument.
self.done=1
response=PCGIResponse(stdout=PCGIPipe(self), stderr=StringIO())
request=HTTPRequest(self.data, self.env, response)
handle(self.server.module, request, response)
def collect_incoming_data(self, data):
self.data.write(data)
def readable(self):
if not self.done:
return 1
def log_request(self, bytes):
if self.env.has_key('HTTP_USER_AGENT'):
user_agent=self.env['HTTP_USER_AGENT']
else:
user_agent=''
if self.env.has_key('HTTP_REFERER'):
referer=self.env['HTTP_REFERER']
else:
referer=''
if self.env.has_key('PATH_INFO'):
path=self.env['PATH_INFO']
else:
path='%s/' % self.server.module
if self.env.has_key('REQUEST_METHOD'):
method=self.env['REQUEST_METHOD']
else:
method="GET"
addr=self.addr
if addr and type(addr) is TupleType:
self.server.logger.log (
addr[0],
'%d - - [%s] "%s %s" %d %d "%s" "%s"' % (
addr[1],
time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.localtime(time.time())
) + tz_for_log,
method, path, self.reply_code, bytes,
referer, user_agent
)
)
else:
self.server.logger.log (
'127.0.0.1',
' - - [%s] "%s %s" %d %d "%s" "%s"' % (
time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.gmtime(time.time())
) + tz_for_log,
method, path, self.reply_code, bytes,
referer, user_agent
)
)
def push(self, producer, send=1):
# this is thread-safe when send is false
# note, that strings are not wrapped in
# producers by default
self.producer_fifo.push(producer)
if send: self.initiate_send()
def __repr__(self):
return "<PCGIChannel at %x>" % id(self)
def close(self):
self.closed=1
while self.producer_fifo:
p=self.producer_fifo.first()
if p is not None and type(p) != StringType:
p.more() # free up resources held by producer
self.producer_fifo.pop()
asyncore.dispatcher.close(self)
class PCGIServer(asyncore.dispatcher):
"""Accepts PCGI requests and hands them off to the PCGIChannel for
handling.
PCGIServer can be configured with either a PCGI info file or by
directly specifying the module, pid_file, and either port (for
inet sockets) or socket_file (for unix domain sockets.)
For inet sockets, the ip argument specifies the address from which
the server will accept connections, '' indicates all addresses. If
you only want to accept connections from the localhost, set ip to
'127.0.0.1'."""
channel_class=PCGIChannel
def __init__ (self,
module='Main',
ip='127.0.0.1',
port=None,
socket_file=None,
pid_file=None,
pcgi_file=None,
resolver=None,
logger_object=None):
self.ip = ip
asyncore.dispatcher.__init__(self)
self.count=counter()
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
if resolver:
self.logger = logger.resolving_logger (resolver, logger_object)
else:
self.logger = logger.unresolving_logger (logger_object)
# get configuration
self.module=module
self.port=port
self.pid_file=pid_file
self.socket_file=socket_file
if pcgi_file is not None:
self.read_info(pcgi_file)
# write pid file
try:
f = open(self.pid_file, 'w')
f.write(str(os.getpid()))
f.close()
except IOError:
self.log_info("Cannot write PID file.", 'error')
# setup sockets
if self.port:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((self.ip, self.port))
self.log_info(
'PCGI Server started at %s\n'
'\tInet socket port: %s' % (time.ctime(time.time()), self.port)
)
else:
try:
os.unlink(self.socket_file)
except os.error:
pass
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(self.socket_file)
try:
os.chmod(self.socket_file,0777)
except os.error:
pass
self.log_info(
'PCGI Server started at %s\n'
'\tUnix socket: %s' % (time.ctime(time.time()), self.socket_file)
)
self.listen(256)
def create_socket(self, family, type):
asyncore.dispatcher.create_socket(self, family, type)
requestCloseOnExec(self.socket)
def read_info(self,info_file):
"read configuration information from a PCGI info file"
lines=open(info_file).readlines()
directives={}
try:
for line in lines:
line=string.strip(line)
if not len(line) or line[0]=='#':
continue
k,v=string.split(line,'=',1)
directives[string.strip(k)]=string.strip(v)
except:
raise 'ParseError', 'Error parsing PCGI info file'
self.pid_file=directives.get('PCGI_PID_FILE',None)
self.socket_file=directives.get('PCGI_SOCKET_FILE',None)
if directives.has_key('PCGI_PORT'):
self.port=string.atoi(directives['PCGI_PORT'])
if directives.has_key('PCGI_MODULE'):
self.module=directives['PCGI_MODULE']
elif directives.has_key('PCGI_MODULE_PATH'):
path=directives['PCGI_MODULE_PATH']
path,module=os.path.split(path)
module,ext=os.path.splitext(module)
self.module=module
def handle_accept (self):
self.count.increment()
try:
conn, addr = self.accept()
except socket.error:
self.log_info('Server accept() threw an exception', 'warning')
return
self.channel_class(self, conn, addr)
def readable(self):
return len(asyncore.socket_map) < CONNECTION_LIMIT
def writable (self):
return 0
def listen(self, num):
# override asyncore limits for nt's listen queue size
self.accepting = 1
return self.socket.listen (num)
class PCGIResponse(HTTPResponse):
def write(self, data):
if not self._wrote:
self.stdout.write(str(self))
self._wrote=1
self.stdout.write(data)
def _finish(self):
self.stdout.finish(self)
self.stdout.close()
self.stdout=None
self._request=None
class PCGIPipe:
"""
Formats a HTTP response in PCGI format
10 digits indicating len of STDOUT
STDOUT
10 digits indicating len of STDERR
STDERR
Note that this implementation never sends STDERR
"""
def __init__(self, channel):
self._channel=channel
self._data=StringIO()
self._shutdown=0
def write(self,text):
self._data.write(text)
def close(self):
if not self._channel.closed:
data=self._data.getvalue()
l=len(data)
DebugLogger.log('A', id(self._channel),
'%s %s' % (self._channel.reply_code, l))
self._channel.push('%010d%s%010d' % (l, data, 0), 0)
self._channel.push(LoggingProducer(self._channel, l, 'log_request'), 0)
self._channel.push(CallbackProducer(
lambda t=('E', id(self._channel)): apply(DebugLogger.log,t)), 0)
if self._shutdown:
try: r=self._shutdown[0]
except: r=0
sys.ZServerExitCode=r
self._channel.push(ShutdownProducer(), 0)
Wakeup(lambda: asyncore.close_all())
else:
self._channel.push(None, 0)
Wakeup()
self._data=None
self._channel=None
def finish(self, response):
if response._shutdownRequested():
self._shutdown = 1
self._channel.reply_code=response.status
=== Added File Zope/lib/python/ZServer/Producers.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer pipe utils. These producers basically function as callbacks.
"""
import asyncore
import sys
class ShutdownProducer:
"shuts down medusa"
def more(self):
asyncore.close_all()
class LoggingProducer:
"logs request"
def __init__(self, logger, bytes, method='log'):
self.logger=logger
self.bytes=bytes
self.method=method
def more(self):
getattr(self.logger, self.method)(self.bytes)
self.logger=None
return ''
class CallbackProducer:
"Performs a callback in the channel's thread"
def __init__(self, callback):
self.callback=callback
def more(self):
self.callback()
self.callback=None
return ''
class file_part_producer:
"producer wrapper for part of a file[-like] objects"
# match http_channel's outgoing buffer size
out_buffer_size = 1<<16
def __init__(self, file, lock, start, end):
self.file=file
self.lock=lock
self.start=start
self.end=end
def more(self):
end=self.end
if not end: return ''
start=self.start
if start >= end: return ''
file=self.file
size=end-start
bsize=self.out_buffer_size
if size > bsize: size=bsize
self.lock.acquire()
try:
file.seek(start)
data = file.read(size)
finally:
self.lock.release()
if data:
start=start+len(data)
if start < end:
self.start=start
return data
self.end=0
del self.file
return data
class file_close_producer:
def __init__(self, file):
self.file=file
def more(self):
file=self.file
if file is not None:
file.close()
self.file=None
return ''
=== Added File Zope/lib/python/ZServer/README.txt ===
ZServer README
--------------
What is ZServer?
ZServer is an integration of the Zope application server and the
Medusa information server. See the ZServer architecture document for
more information::
http://www.zope.org/Documentation/Reference/ZServer
ZServer gives you HTTP, FTP, WebDAV, PCGI, and remote interactive
Python access. In later releases it will probably offer more
protocols such as FastCGI, etc.
What is Medusa?
Medusa is a Python server framework with uses a single threaded
asynchronous sockets approach. For more information see::
http://www.nightmare.com/medusa
There's also an interesting Medusa tutorial at::
http://www.nightmare.com:8080/nm/apps/medusa/docs/programming.html
ZServer HTTP support
ZServer offers HTTP 1.1 publishing for Zope. It does not support
publishing files from the file system. You can specify the HTTP port
using the -w command line argument for the z2.py start script. You
can also specify CGI environment variables on the command line using
z2.py
ZServer FTP support
What you can do with FTP
FTP access to Zope allows you to FTP to the Zope object hierarchy
in order to perform managerial tasks. You can:
* Navigate the object hierarchy with 'cd'
* Replace the content of Documents, Images, and Files
* Create Documents, Images, Files, Folders
* Delete objects and Folders.
So basically you can do more than is possible with HTTP PUT. Also,
unlike PUT, FTP gives you access to Document content. So when you
download a Document you are getting its content, not what it looks
like when it is rendered.
Using FTP
To FTP into Zope, ZServer must be configured to serve FTP. By
default ZServer serves FTP on port 9221. So to connect to Zope you
would issue a command like so::
$ ftp localhost 9221
When logging in to FTP, you have some choices. You can connect
anonymously by using a username of 'anonymous' and any password.
Or you can login as a Zope user. Since Zope users are defined at
different locations in the object hierarchy, authentication can be
problematic. There are two solutions:
* login and then cd to the directory where you are defined.
* login with a special name that indicates where you are
defined.
The format of the special name is '<username>@<path>'. For
example::
joe@Marketing/Projects
FTP permissions
FTP support is provided for Folders, Documents, Images, and Files.
You can control access to FTP via the new 'FTP access' permission.
This permission controls the ability to 'cd' to a Folder and to
download objects. Uploading and deleting and creating objects are
controlled by existing permissions.
FTP limits
You can set limits for the number of simultaneous FTP connections.
You can separately configure the number of anonymous and
authenticated connections. Right now this setting is set in
'ZServerFTP.py'. In the future, it may be more easy to configure.
Properties and FTP: The next step
The next phase of FTP support will allow you to edit properties of
all Zope objects. Probably properties will be exposed via special
files which will contain an XML representation of the object's
properties. You could then download the file, edit the XML and
upload it to change the object's properties.
We do not currently have a target date for FTP property support.
How does FTP work?
The ZServer's FTP channel object translates FTP requests into
ZPublisher requests. The FTP channel then analyses the response
and formulates an appropriate FTP response. The FTP channel
stores some state such as the current working directory and the
username and password.
On the Zope side of things, the 'lib/python/OFS/FTPInterface.py'
module defines the Zope FTP interface, for listing sub-items,
stating, and getting content. The interface is implemented in
'SimpleItem', and in other Zope classes. Programmers will not
need to implement the entire interface if they inherit from
'SimpleItem'. All the other FTP functions are handled by
existing methods like 'manage_delObjects', and 'PUT', etc.
ZServer PCGI support
ZServer will service PCGI requests with both inet and unix domain
sockets. This means you can use ZServer instead of
'pcgi_publisher.py' as your long running PCGI server process. In the
future, PCGI may be able to activate ZServer.
Using PCGI instead of HTTP allows you to forward requests from
another web server to ZServer. The CGI environment and HTTP headers
are controlled by the web server, so you don't need to worry about
managing the ZServer environment. However, this configuration will
impose a larger overhead than simply using the web server as an HTTP
proxy for ZServer.
To use PCGI, configure your PCGI info files to communicate with
ZServer by setting the PCGI_PORT, PCGI_SOCKET_FILE, and PCGI_NAME.
The other PCGI settings are currently ignored by ZServer.
ZServer's PCGI support will work with mod_pcgi.
ZServer monitor server
ZServer now includes the Medusa monitor server. This basically gives
you a remote, secure Python prompt. You can interactively access Zope.
This is a very powerful, but dangerous tool. Be careful.
To use the monitor server specify a monitor port number using the -m
option with the z2.py start script. The default port is 9999.
To connect to the monitor server use the 'ZServer/medusa/monitor_client.py'
or 'ZServer/medusa/monitor_client_win32.py' script. For example::
$ python2.1 ZServer/medusa/monitor_client.py localhost 9999
You will then be asked to enter a password. This is the Zope super manager
password which is stored in the 'access' file.
Then you will be greeted with a Python prompt. To access Zope import
the Zope module::
>>> import Zope
The Zope top level Zope object is available via the 'Zope.app' function::
>>> a=Zope.app()
From this object you can reach all other Zope objects as subobjects.
Remember if you make changes to Zope objects and want those changes to be
saved you need to commmit the transaction::
>>> get_transaction().commit()
ZServer WebDAV support
WebDAV is a new protocol for managing web resources. WebDAV operates
over HTTP. Since WebDAV uses HTTP, ZServer doesn't really have to do
anything special, except stay out of Zope's way when handling WebDAV
requests.
The only major WebDAV client at this time is Internet Explorer 5. It
works with Zope.
Differences between ZopeHTTPServer and ZServer
ZopeHTTPServer is old and no longer being actively maintained.
Both ZopeHTTPServer and ZServer are Python HTTP servers.
ZopeHTTPServer is built on the standard Python SimpleHTTPServer
framework. ZServer is built on Medusa.
ZopeHTTPServer is very limited. It can only publish one module at a
time. It can only publish via HTTP. It has no support for thread
pools.
ZServer on the other hand is more complex and supports publishing
multiple modules, thread pools, and it uses a new threaded
architecture for accessing ZPublisher.
Running ZServer as nobody
Normally ZServer will run with the userid of the user who starts
it. However, if ZServer is started by root, it will attempt to
become nobody or any userid you specify with the -u argument to the
z2.py start script.
ZServer is similar to ZopeHTTPServer in these respects.
If you run Zope with different userids you must be aware of
permission issues. Zope must be able to read and write to the 'var'
directory. If you change the userid Zope is running under you will
probably need to change the permissions on the 'var' directory
and the files in it in order for Zope to run under a different
userid.
Support
Questions and comments should go to 'support@digicool.com'.
You can report bugs and check on the status of bugs using the Zope
bug collector::
http://www.zope.org/Resources/Collector/
License
ZServer is covered by the ZPL despite the fact that it comes with
much of the Medusa source code. The portions of Medusa that come
with ZServer are licensed under the ZPL.
Outstanding issues
The FTP interface for Zope objects may be changed.
HTTP 1.1 support is ZServer is incomplete, though it should work for
most HTTP 1.1 clients.
=== Added File Zope/lib/python/ZServer/WebDAVSrcHandler.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Special HTTP handler which forces GET requests to return the "source"
of documents (uses 'manage_FTPget'). Works around current WebDAV
clients' failure to implement the "source-link" feature of the spec.
"""
__version__ = "1.0"
from HTTPServer import zhttp_handler
import os
class WebDAVSrcHandler( zhttp_handler ):
"""
"""
def get_environment( self, request ):
"""
Munge the request to ensure that we call manage_FTPGet.
"""
env = zhttp_handler.get_environment( self, request )
# Set a flag to indicate this request came through the WebDAV source
# port server.
env['WEBDAV_SOURCE_PORT'] = 1
if env['REQUEST_METHOD'] == 'GET':
path_info = env['PATH_INFO']
path_info = os.path.join( path_info, 'manage_FTPget' )
path_info = os.path.normpath( path_info )
if os.sep != '/':
path_info = path_info.replace( os.sep, '/' )
env['PATH_INFO'] = path_info
# Workaround for lousy WebDAV implementation of M$ Office 2K.
# Requests for "index_html" are *sometimes* send as "index_html."
# We check the user-agent and remove a trailing dot for PATH_INFO
# and PATH_TRANSLATED
if env.get("HTTP_USER_AGENT","").find("Microsoft Data Access Internet Publishing Provider")>-1:
if env["PATH_INFO"][-1]=='.':
env["PATH_INFO"] = env["PATH_INFO"][:-1]
if env["PATH_TRANSLATED"][-1]=='.':
env["PATH_TRANSLATED"] = env["PATH_TRANSLATED"][:-1]
return env
=== Added File Zope/lib/python/ZServer/ZService.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer as a NT service.
The serice starts up and monitors a ZServer process.
Features:
* When you start the service it starts ZServer
* When you stop the serivice it stops ZServer
* It monitors ZServer and restarts it if it exits abnormally
* If ZServer is shutdown from the web, the service stops.
* If ZServer cannot be restarted, the service stops.
Usage:
Installation
The ZServer service should be installed by the Zope Windows
installer. You can manually install, uninstall the service from
the commandline.
ZService.py [options] install|update|remove|start [...]
|stop|restart [...]|debug [...]
Options for 'install' and 'update' commands only:
--username domain\username : The Username the service is to run
under
--password password : The password for the username
--startup [manual|auto|disabled] : How the service starts,
default = manual
Commands
install : Installs the service
update : Updates the service, use this when you change
ZServer.py
remove : Removes the service
start : Starts the service, this can also be done from the
services control panel
stop : Stops the service, this can also be done from the
services control panel
restart : Restarts the service
debug : Runs the service in debug mode
You can view the usage options by running ZServer.py without any
arguments.
Note: you may have to register the Python service program first,
win32\pythonservice.exe /register
Starting Zope
Start Zope by clicking the 'start' button in the services control
panel. You can set Zope to automatically start at boot time by
choosing 'Auto' startup by clicking the 'statup' button.
Stopping Zope
Stop Zope by clicking the 'stop' button in the services control
panel. You can also stop Zope through the web by going to the
Zope control panel and by clicking 'Shutdown'.
Event logging
Zope events are logged to the NT application event log. Use the
event viewer to keep track of Zope events.
Registry Settings
You can change how the service starts ZServer by editing a registry
key.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
<Service Name>\Parameters\start
The value of this key is the command which the service uses to
start ZServer. For example:
"C:\Program Files\Zope\bin\python.exe"
"C:\Program Files\Zope\z2.py" -w 8888
TODO:
* Integrate it into the Windows installer.
* Add ZLOG logging in addition to event log logging.
* Make it easier to run multiple Zope services with one Zope install
This script does for NT the same sort of thing zdaemon.py does for UNIX.
Requires Python win32api extensions.
"""
import sys, os, time, imp
# Some fancy path footwork is required here because we
# may be run from python.exe or lib/win32/PythonService.exe
home=os.path.split(os.path.split(sys.executable)[0])[0]
if sys.executable[-10:]!='python.exe':
home=os.path.split(home)[0]
home=os.path.split(home)[0]
sys.path.append(os.path.join(home, 'bin'))
sys.path.append(os.path.join(home, 'ZServer'))
sys.path.append(os.path.join(home, 'bin', 'lib', 'win32'))
sys.path.append(os.path.join(home, 'bin', 'lib', 'win32', 'lib'))
# pythoncom and pywintypes are special, and require these hacks when
# we dont have a standard Python installation around.
import win32api
def magic_import(modulename, filename):
# by Mark Hammond
try:
# See if it does import first!
return __import__(modulename)
except ImportError:
pass
# win32 can find the DLL name.
h = win32api.LoadLibrary(filename)
found = win32api.GetModuleFileName(h)
# Python can load the module
mod = imp.load_module(modulename, None, found, ('.dll', 'rb', imp.C_EXTENSION))
# inject it into the global module list.
sys.modules[modulename] = mod
# And finally inject it into the namespace.
globals()[modulename] = mod
win32api.FreeLibrary(h)
magic_import('pywintypes','pywintypes21.dll')
import win32serviceutil, win32service, win32event, win32process
try: import servicemanager
except: pass
class ZServerService(win32serviceutil.ServiceFramework):
# Some trickery to determine the service name. The WISE
# installer will write an svcname.txt to the ZServer dir
# that we can use to figure out our service name.
path=os.path.join(home, 'ZServer', 'svcname.txt')
file=open(path, 'r')
_svc_name_=file.readline().strip()
file.close()
_svc_display_name_ = "Zope (%s)" % _svc_name_
restart_min_time=5 # if ZServer restarts before this many
# seconds then we have a problem, and
# need to stop the service.
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def SvcDoRun(self):
self.start_zserver()
while 1:
rc=win32event.WaitForMultipleObjects(
(self.hWaitStop, self.hZServer), 0, win32event.INFINITE)
if rc - win32event.WAIT_OBJECT_0 == 0:
break
else:
self.restart_zserver()
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING, 5000)
def SvcStop(self):
servicemanager.LogInfoMsg('Stopping Zope.')
try:
self.stop_zserver()
except:
pass
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
def start_zserver(self):
sc=self.get_start_command()
result=win32process.CreateProcess(None, self.get_start_command(),
None, None, 0, 0, None, None, win32process.STARTUPINFO())
self.hZServer=result[0]
self.last_start_time=time.time()
servicemanager.LogInfoMsg('Starting Zope.')
def stop_zserver(self):
win32process.TerminateProcess(self.hZServer,0)
def restart_zserver(self):
if time.time() - self.last_start_time < self.restart_min_time:
servicemanager.LogErrorMsg('Zope died and could not be restarted.')
self.SvcStop()
code=win32process.GetExitCodeProcess(self.hZServer)
if code == 0:
# Exited with a normal status code,
# assume that shutdown is intentional.
self.SvcStop()
else:
servicemanager.LogWarningMsg('Restarting Zope.')
self.start_zserver()
def get_start_command(self):
return win32serviceutil.GetServiceCustomOption(self,'start')
def set_start_command(value):
"sets the ZServer start command if the start command is not already set"
current=win32serviceutil.GetServiceCustomOption(ZServerService,
'start', None)
if current is None:
win32serviceutil.SetServiceCustomOption(ZServerService,'start',value)
if __name__=='__main__':
win32serviceutil.HandleCommandLine(ZServerService)
if 'install' in sys.argv:
command='"%s" "%s" -S' % (sys.executable, os.path.join(home,'z2.py'))
set_start_command(command)
print "Setting ZServer start command to:", command
=== Added File Zope/lib/python/ZServer/__init__.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import sys, os
# HACKERY to get around asyncore issues. This ought to go away! We're
# currently using the Python 2.2 asyncore bundled with Zope to override
# brokenness in the Python 2.1 version. We need to do some funny business
# to make this work, as a 2.2-ism crept into the asyncore code.
if os.name == 'posix':
import fcntl
if not hasattr(fcntl, 'F_GETFL'):
import FCNTL
fcntl.F_GETFL = FCNTL.F_GETFL
fcntl.F_SETFL = FCNTL.F_SETFL
from medusa import asyncore
sys.modules['asyncore'] = asyncore
from medusa.test import max_sockets
CONNECTION_LIMIT=max_sockets.max_select_sockets()
ZSERVER_VERSION='1.1b1'
import App.FindHomes
try:
import App.version_txt
ZOPE_VERSION=App.version_txt.version_txt()
except:
ZOPE_VERSION='experimental'
# Try to poke zLOG default logging into asyncore
# XXX We should probably should do a better job of this,
# however that would mean that ZServer required zLOG.
try:
from zLOG import LOG, register_subsystem, BLATHER, INFO, WARNING, ERROR
register_subsystem('ZServer')
severity={'info':INFO, 'warning':WARNING, 'error': ERROR}
def log_info(self, message, type='info'):
if message[:14]=='adding channel' or \
message[:15]=='closing channel' or \
message == 'Computing default hostname':
LOG('ZServer', BLATHER, message)
else:
LOG('ZServer', severity[type], message)
import asyncore
asyncore.dispatcher.log_info=log_info
except:
pass
# A routine to try to arrange for request sockets to be closed
# on exec. This makes it easier for folks who spawn long running
# processes from Zope code. Thanks to Dieter Maurer for this.
try:
import fcntl
try:
from fcntl import F_SETFD, FD_CLOEXEC
except ImportError:
from FCNTL import F_SETFD, FD_CLOEXEC
def requestCloseOnExec(sock):
try: fcntl.fcntl(sock.fileno(), F_SETFD, FD_CLOEXEC)
except: pass
except (ImportError, AttributeError):
def requestCloseOnExec(sock):
pass
import asyncore
from medusa import resolver, logger
from HTTPServer import zhttp_server, zhttp_handler
from PCGIServer import PCGIServer
from FCGIServer import FCGIServer
from FTPServer import FTPServer
from PubCore import setNumberOfThreads
from medusa.monitor import secure_monitor_server
# override the service name in logger.syslog_logger
logger.syslog_logger.svc_name='ZServer'