Commit ea095f40 authored by Amos Latteier's avatar Amos Latteier

New ZServer architecture. Major updates all over the place. We're getting close to beta

quality.
parent 83837d35
......@@ -6,6 +6,52 @@ ZServer Changes
Releases
ZServer 1.0b1
New Features
Major code reorganization. Major architecture changes. Future
producers are gone. Use of special request and response objects
to do most of the heavy lifting.
ZServer can now handle multiple HTTP requests per connection.
Now using (almost) unmodified Medusa sources.
Added environment variable overriding to HTTPServer.py
Bugs Fixed
Using PCGI under IIS, now PATH_INFO begins with a /
Fixed FTP passive mode response message IP address.
Known Bugs
PCGI under IIS 4.0 is broken with respect to file uploads, and binary
data in general. IIS seems to convert \n to \r\n in binary data :-(
The latest medusa select_trigger.py doesn't seem to work under NT.
TODO
stress test multi-threaded channel push. (done)
test http 1.1 pipelined requests. (done)
iron out additional http 1.1 issues... (hmm)
test ftp with emacs. (done)
remove cruft.
fix medusa select_trigger under win32.
re-write Zope installers to install a ZServer start script.
get pcgi working with IIS and apache under win32, or at least get
it working under apache 1.3.6 on NT
ZServer 1.0a2
New Features
......@@ -23,7 +69,7 @@ ZServer Changes
and defer authentication until you cd to a directory where you
are defined.
Bug Fixed
Bugs Fixed
Data was being dropped from POST requests because of a bug in
the sized input collection of asynchat.
......
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
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 regsub import gsub
from base64 import encodestring
import string
class FTPRequest(HTTPRequest):
def __init__(self, path, command, channel, response, stdin=None):
if stdin is None:
stdin=StringIO()
env=self._get_env(path, command, channel, stdin)
HTTPRequest.__init__(self, stdin, env, response, clean=1)
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' % gsub('\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=='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)
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
Response class for the FTP Server.
"""
from ZPublisher.HTTPResponse import HTTPResponse
from PubCore.ZEvent import Wakeup
from cStringIO import StringIO
import marshal
class FTPResponse(HTTPResponse):
"""
Response to an FTP command
"""
def _finish(self):
self.stdout.finish(self)
def _marshalledBody(self):
return marshal.loads(self.body)
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
def write(self, text):
pass
def close(self):
pass
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,))
# is this necessary to break cycles?
self._callback=None
self._response=None
self._args=None
return result
def make_response(callback,*args):
# XXX should this be the FTPResponse constructor instead?
return FTPResponse(stdout=CallbackPipe(callback, args), stderr=StringIO())
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
ZServer HTTPResponse
The HTTPResponse class takes care of server headers, response munging
and logging duties.
"""
import time, regex, string
from cStringIO import StringIO
from ZPublisher.HTTPResponse import HTTPResponse, end_of_header_search
from medusa.http_date import build_http_date
from PubCore.ZEvent import Wakeup
from medusa.producers import hooked_producer
from medusa import http_server
from Producers import ShutdownProducer, LoggingProducer, CallbackProducer
__version__='1.0b1'
__zope_version__='1.11a1' # XXX retrieve this somehow
# XXX there should be a method somewhere for this
class ZServerHTTPResponse(HTTPResponse):
"Used to push data into a channel's producer fifo"
http_chunk=0
http_chunk_size=1024
def __str__(self,
html_search=regex.compile('<html>',regex.casefold).search,
):
if self._wrote: return '' # Streaming output was used.
headers=self.headers
body=self.body
if body:
isHTML=self.isHTML(body)
if not headers.has_key('content-type'):
if isHTML:
c='text/html'
else:
c='text/plain'
self.setHeader('content-type',c)
else:
isHTML = headers['content-type']=='text/html'
if isHTML and end_of_header_search(self.body) < 0:
lhtml=html_search(body)
if lhtml >= 0:
lhtml=lhtml+6
body='%s<head></head>\n%s' % (body[:lhtml],body[lhtml:])
else:
body='<html><head></head>\n' + body
self.setBody(body)
body=self.body
if not headers.has_key('content-type') and self.status == 200:
self.setStatus('nocontent')
if not headers.has_key('content-length'):
self.setHeader('content-length',len(body))
headersl=[]
append=headersl.append
status=headers.get('status', '200 OK')
# status header must come first.
append("HTTP/%s: %s" % (self._http_version, status))
if headers.has_key('status'):
del headers['status']
# add zserver headers
append('Server: Zope/%s ZServer/%s' % (__zope_version__, __version__))
append('Date: %s' % build_http_date(time.time()))
append('X-Powered-By: Zope (www.zope.org), Python (www.python.org)')
chunk=0
if self._http_version=='1.0':
if self._http_connection=='keep alive':
if self.headers.has_key('content-length'):
self.setHeader('Connection','close')
else:
self.setHeader('Connection','Keep-Alive')
else:
self.setHeader('Connection','close')
elif self._http_version=='1.1':
if self._http_connection=='close':
self.setHeader('Connection','close')
elif not self.headers.has_key('content-length'):
if self.headers.has_key('transfer-encoding'):
if self.headers['transfer-encoding'] != 'chunked':
self.setHeader('Connection','close')
else:
chunk=1
elif self.http_chunk:
self.setHeader('Transfer-Encoding','chunked')
chunk=1
else:
self.setHeader('Connection','close')
if chunk:
chunked_body=''
while body:
chunk=body[:self.http_chunk_size]
body=body[self.http_chunk_size:]
chunked_body='%s%x\r\n%s\r\n' % (chunked_body, len(chunk), chunk)
chunked_body='%s0\r\n\r\n' % chunked_body
body=chunked_body
for key, val in headers.items():
if string.lower(key)==key:
# only change non-literal header names
key="%s%s" % (string.upper(key[:1]), key[1:])
start=0
l=string.find(key,'-',start)
while l >= start:
key="%s-%s%s" % (key[:l],string.upper(key[l+1:l+2]),key[l+2:])
start=l+1
l=string.find(key,'-',start)
append("%s: %s" % (key, val))
if self.cookies:
headersl=headersl+self._cookie_list()
headersl[len(headersl):]=[self.accumulated_headers, body]
return string.join(headersl,'\r\n')
# XXX add server headers, etc to write()
def _finish(self):
self.stdout.finish(self)
self.stdout.close()
self.stdout=None # need to break cycle?
self._request=None
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):
self._bytes=self._bytes + len(text)
self._channel.push(text,0)
Wakeup()
def close(self):
self._channel.push(LoggingProducer(self._request, self._bytes), 0)
self._channel.push(CallbackProducer(self._channel.done))
if self._shutdown:
self._channel.push(ShutdownProducer(), 0)
elif self._close:
self._channel.push(None, 0)
Wakeup()
self._channel=None #need to break cycles?
self._request=None
def finish(self, request):
if request.headers.get('bobo-exception-type', '') == \
'exceptions.SystemExit':
self._shutdown=1
if request.headers.get('connection','')=='close':
self._close=1
self._request.reply_code=request.status
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
response._http_connection=string.lower(
http_server.get_header(http_server.CONNECTION, request.header))
return response
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
......@@ -17,12 +19,13 @@
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
......@@ -73,27 +76,6 @@
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
......@@ -102,27 +84,40 @@
##############################################################################
"""
Zope Medusa Handler
Medusa HTTP server for Zope
Uses a new threaded architecture.
Works with http_server.py
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 regex
import string
import os
import types
from cStringIO import StringIO
import thread
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, http_channel
from medusa import counter, producers, asyncore
from medusa.default_handler import split_path, unquote, get_header
from medusa.producers import NotReady
CONTENT_LENGTH = regex.compile('Content-Length: \([0-9]+\)',regex.casefold)
CONNECTION = regex.compile ('Connection: \(.*\)', regex.casefold)
......@@ -135,20 +130,24 @@ header2env={'content-length' : 'CONTENT_LENGTH',
}
class zope_handler:
"publishes a module with ZPublisher"
class zhttp_handler:
"A medusa style handler for zhttp_server"
# XXX add code to allow env overriding
def __init__ (self, module, uri_base=None):
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
......@@ -166,7 +165,7 @@ class zope_handler:
uri_regex='%s.*' % self.uri_base
self.uri_regex = regex.compile(uri_regex)
def match (self, request):
def match(self, request):
uri = request.uri
if self.uri_regex.match(uri) == len(uri):
return 1
......@@ -204,11 +203,11 @@ class zope_handler:
# ZPublisher doesn't want the leading '?'
query = query[1:]
env = {}
env['REQUEST_METHOD'] = string.upper(request.command)
env['SERVER_PORT'] = str(request.channel.server.port)
env['SERVER_NAME'] = request.channel.server.server_name
env['SERVER_SOFTWARE'] = request['Server']
env['SERVER_PROTOCOL']='HTTP/1.0' # should this be 1.1?
env['REQUEST_METHOD']=string.upper(request.command)
env['SERVER_PORT']=str(request.channel.server.port)
env['SERVER_NAME']=request.channel.server.server_name
env['SERVER_SOFTWARE']=request['Server']
env['SERVER_PROTOCOL']=request.version
if self.uri_base=='/':
env['SCRIPT_NAME']=''
env['PATH_INFO']=path
......@@ -243,62 +242,20 @@ class zope_handler:
string.join(string.split(key, "-"), "_"))
if value and not env.has_key(key):
env[key]=value
for key, value in self.env_override.items():
env[key]=value
return env
def continue_request(self,sin,request):
def continue_request(self, sin, request):
"continue handling request now that we have the stdin"
outpipe=handle(self.module_name,self.get_environment(request),sin)
# now comes the hairy stuff. adapted from http_server
#
connection = string.lower(get_header(CONNECTION,request.header))
close_it = 0
wrap_in_chunking = 0
if request.version == '1.0':
if connection == 'keep-alive':
if not request.has_key ('Content-Length'):
close_it = 1
else:
request['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif request.version == '1.1':
if connection == 'close':
close_it = 1
elif not request.has_key ('Content-Length'):
if request.has_key ('Transfer-Encoding'):
if not request['Transfer-Encoding'] == 'chunked':
close_it = 1
elif request.use_chunked:
request['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
else:
close_it = 1
if close_it:
request['Connection'] = 'close'
outgoing_producer = header_scanning_producer(request,outpipe)
# apply a few final transformations to the output
request.channel.push_with_producer (
# hooking lets us log the number of bytes sent
producers.hooked_producer (
outgoing_producer,
request.log
)
)
request.channel.current_request = None
if close_it:
request.channel.close_when_done()
def status (self):
env=self.get_environment(request)
zresponse=make_response(request,env)
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>
......@@ -325,48 +282,39 @@ class zope_handler:
self.continue_request(d,r)
class header_scanning_producer:
"""This weird producer patches together
medusa's idea of http headers with ZPublisher's
"""
def __init__(self,request,pipe):
self.request=request
self.pipe=pipe
self.done=None
self.buffer=''
self.exit=None
def more(self):
if self.buffer:
b=self.buffer
self.buffer=''
return b
data=self.pipe.read()
if data is None:
raise NotReady()
if data=='' and self.exit:
asyncore.close_all()
return data
def ready(self):
if self.done:
return self.pipe.ready()
elif self.pipe.ready():
self.buffer=self.buffer+self.pipe.read()
if string.find(self.buffer,"\n\n") != -1:
[headers,html]=string.split(self.buffer,"\n\n",1)
headers=string.split(headers,"\n")
for line in headers:
[header, header_value]=string.split(line,": ",1)
if header=="Status":
[code,message]=string.split(header_value," ",1)
self.request.reply_code=string.atoi(code)
elif header=="Bobo-Exception-Type" and \
header_value=="exceptions.SystemExit":
self.exit=1
else:
self.request[header]=header_value
self.buffer=self.request.build_reply_header()+html
del self.request
self.done=1
class zhttp_channel(http_channel):
"http channel"
def __init__(self, server, conn, addr):
http_channel.__init__(self, server, conn, addr)
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
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
module_name, request, response=self.queue[0]
self.queue=self.queue[1:]
handle(module_name, request, response)
def done(self):
"Called when a pushing request is finished"
self.working=0
self.work()
class zhttp_server(http_server):
"http server"
channel_class = zhttp_channel
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
......@@ -17,12 +19,13 @@
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
......@@ -73,27 +76,6 @@
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
......@@ -101,7 +83,8 @@
#
##############################################################################
"""First cut at a Medusa PCGI server.
"""
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
......@@ -119,15 +102,22 @@ Note that ZServer can operate multiple PCGI servers.
from medusa import asynchat, asyncore, logger
from medusa.counter import counter
from medusa.producers import NotReady, hooked_producer
from medusa.http_server import compute_timezone_for_log
from PubCore import handle
from PubCore.ZEvent import Wakeup
from ZPublisher.HTTPResponse import HTTPResponse
from ZPublisher.HTTPRequest import HTTPRequest
from Producers import ShutdownProducer, LoggingProducer
from cStringIO import StringIO
from tempfile import TemporaryFile
import socket
import string
import os
import time
tz_for_log=compute_timezone_for_log()
class PCGIChannel(asynchat.async_chat):
"""Processes a PCGI request by collecting the env and stdin and
......@@ -138,10 +128,10 @@ class PCGIChannel(asynchat.async_chat):
self.server = server
self.addr = addr
asynchat.async_chat.__init__ (self, sock)
self.env={}
self.data=StringIO()
self.set_terminator(10)
self.size=None
self.env={}
self.done=None
def found_terminator(self):
......@@ -150,6 +140,7 @@ class PCGIChannel(asynchat.async_chat):
# and prepare to read env or stdin
self.data.seek(0)
self.size=string.atoi(self.data.read())
print "got size", self.size
self.set_terminator(self.size)
if self.size==0:
self.set_terminator('\r\n')
......@@ -179,13 +170,14 @@ class PCGIChannel(asynchat.async_chat):
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.env['PATH_INFO'] = '/' + string.join(path[len(script):],'/')
print "REQUEST", self.env['REQUEST_METHOD']
self.data=StringIO()
# now read the next size header
self.set_terminator(10)
else:
# we're done, we've got both env and stdin
print "got stdin", len(self.data.getvalue())
self.set_terminator('\r\n')
self.data.seek(0)
self.send_response()
......@@ -195,11 +187,9 @@ class PCGIChannel(asynchat.async_chat):
# and requesting a callback of self.log with the module
# name and PATH_INFO as an argument.
self.done=1
outpipe=handle(self.server.module, self.env, self.data)
self.push_with_producer(
hooked_producer(PCGIProducer(outpipe), self.log_request)
)
self.close_when_done()
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)
......@@ -209,20 +199,33 @@ class PCGIChannel(asynchat.async_chat):
return 1
def log_request(self, bytes):
# XXX need to add reply code logging
if self.env.has_key('PATH_INFO'):
path='%s/%s' % (self.server.module, self.env['PATH_INFO'])
path='%s%s' % (self.server.module, 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"
self.server.logger.log (
self.addr[0],
'%d - - [time goes here] "%s" %d' % (
self.addr[1], path, bytes
'%d - - [%s] "%s %s" %d' % (
self.addr[1],
time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.gmtime(time.time())
) + tz_for_log,
method, path, bytes
)
)
def writable(self):
return len(self.ac_out_buffer) or self.producer_fifo.ready() \
or (not self.connected)
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()
class PCGIServer(asyncore.dispatcher):
......@@ -271,7 +274,7 @@ class PCGIServer(asyncore.dispatcher):
# write pid file
try:
f = open(self.pid_file, 'wb')
f = open(self.pid_file, 'w')
f.write(str(os.getpid()))
f.close()
except IOError:
......@@ -329,42 +332,45 @@ class PCGIServer(asyncore.dispatcher):
return 0
class PCGIProducer:
"""Producer wrapper over output pipe
class PCGIResponse(HTTPResponse):
output format is in PCGI format: 10 digits indicating len of
STDOUT followed by STDOUT.
def _finish(self):
self.stdout.finish(self)
self.stdout.close()
Note: no STDERR is returned.
Note: streaming is not supported, since PCGI requires the length
of the STDOUT be known before it is sent.
class PCGIPipe:
"""
Formats a HTTP response in PCGI format
def __init__(self,pipe):
self.pipe=pipe
self.buffer=StringIO()
self.buffer.write('0'*10) #this will hold length header
self.chunk_size=512
self.done=None
10 digits indicating len of STDOUT
STDOUT
10 digits indicating len of STDERR
STDERR
def ready(self):
if self.done:
return 1
data=self.pipe.read()
if data is not None:
self.buffer.write(data)
if data=='':
self.buffer.write('0'*10) #write stderr header
self.buffer.seek(0)
size=len(self.buffer.getvalue())-20
self.buffer.write('%010d' % size)
self.buffer.seek(0)
self.done=1
return 1
def more(self):
if not self.done:
raise NotReady
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):
data=self._data.getvalue()
l=len(data)
self._channel.push('%010d%s%010d' % (l, data, 0), 0)
self._channel.push(LoggingProducer(self._channel, l, 'log_request'), 0)
if self._shutdown:
self._channel.push(ShutdownProducer(), 0)
else:
return self.buffer.read(self.chunk_size)
self._channel.push(None, 0)
Wakeup()
self._channel=None
def finish(self,request):
if request.headers.get('bobo-exception-type','') == \
'exceptions.SystemExit':
self._shutdown=1
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
ZServer pipe utils. These producers basically function as callbacks.
"""
from medusa 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)
return ''
class CallbackProducer:
"Performs a callback in the channel's thread"
def __init__(self, callback):
self.callback=callback
def more(self):
self.callback()
return ''
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""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.
Optionally, users can login with a special username which indicates
where they are defined. Their login will then be authenticated in
the indicated directory, and they will not be considered anonymous.
The form of the name is '<username>@<path>' where path takes the form
'<folder id>[/<folder id>...]' For example: 'amos@Foo/Bar' This will
authenticate the user 'amos' in the directory '/Foo/Bar'. In addition
the user's FTP session will be rooted in the authenticated directory,
i.e. they will not be able to cd out of the directory.
The main reason to use the rooted FTP login, is to allow non-anonymous
logins. This may be handy, if for example, you disallow anonymous logins,
or if you set the limit for simultaneous anonymous logins very low.
"""
from PubCore import handle
from medusa.ftp_server import ftp_channel, ftp_server
from medusa import asyncore, asynchat, filesys
from medusa.producers import NotReady
from cStringIO import StringIO
import string
import os
from regsub import gsub
from base64 import encodestring
from mimetypes import guess_type
import marshal
import stat
import time
class zope_ftp_channel(ftp_channel):
"Passes its commands to Zope, not a filesystem"
read_only=0
anonymous=1
def __init__ (self, server, conn, addr, module):
ftp_channel.__init__(self,server,conn,addr)
self.module=module
self.userid=''
self.password=''
self.path='/'
def _get_env(self):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % self.module
env['PATH_INFO']=self.path
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=self.server.SERVER_IDENT
if self.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % gsub('\012','',
encodestring('%s:%s' % (self.userid,self.password)))
env['BOBO_DEBUG_MODE']='1'
env['SERVER_NAME']=self.server.hostname
env['SERVER_PORT']=str(self.server.port)
env['REMOTE_ADDR']=self.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# XXX etcetera -- probably set many of these at the start, rather
# than for each request...
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
def make_response(self,resp):
self.log ('==> %s' % resp)
return resp + '\r\n'
# Overriden async_chat methods
writable=asynchat.async_chat.writable_future
# Overriden ftp_channel methods
def cmd_nlst (self, line):
'give name list of files in directory'
self.push_with_producer(self.get_dir_list (line, 0))
def cmd_list (self, line):
'give list files in a directory'
# handles files as well as directories.
# XXX also should maybe handle globbing, yuck.
self.push_with_producer(self.get_dir_list (line, 1))
def get_dir_list (self, line, long=0):
# we need to scan the command line for arguments to '/bin/ls'...
# XXX clean this up, maybe with getopts
if len(line) > 1:
args = string.split(line[1])
else:
args =[]
path_args = []
for arg in args:
if arg[0] != '-':
path_args.append (arg)
else:
if 'l' in arg:
long=1
if len(path_args) < 1:
dir = '.'
else:
dir = path_args[0]
return self.listdir (dir, long)
def listdir (self, path, long=0):
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_FTPlist')
outpipe=handle(self.module,env,StringIO())
return ResponseProducer(outpipe,self._do_listdir,(long,))
def _do_listdir(self,long,response):
code=response.headers['Status'][:3]
if code=='200':
if self.anonymous and not self.userid=='anonymous':
self.anonymous=None
dir_list=''
file_infos=marshal.loads(response.content)
if type(file_infos[0])==type(''):
file_infos=(file_infos,)
if long:
for id, stat_info in file_infos:
dir_list=dir_list+filesys.unix_longify(id,stat_info)+'\r\n'
else:
for id, stat_info in file_infos:
dir_list=dir_list+id+'\r\n'
self.make_xmit_channel()
self.client_dc.push(dir_list)
self.client_dc.close_when_done()
return self.make_response(
'150 Opening %s mode data connection for file list' % (
self.type_map[self.current_mode]
)
)
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Could not list directory.')
def cmd_cwd (self, line):
'change working directory'
# try to call manage_FTPlist on the path
env=self._get_env()
path=line[1]
path=self._join_paths(self.path,path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_cwd,(path[:-15],)))
def _cmd_cwd(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('550 No such directory.')
self.path=path or '/'
return self.make_response('250 CWD command successful.')
if self.anonymous and not self.userid=='anonymous':
self.anonymous=None
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 No such directory.')
def cmd_cdup (self, line):
'change to parent of current working directory'
self.cmd_cwd((None,'..'))
def cmd_pwd (self, line):
'print the current working directory'
self.respond (
'257 "%s" is the current directory.' % (
self.path
)
)
cmd_xpwd=cmd_pwd
def cmd_mdtm (self, line):
'show last modification time of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mdtm))
def _cmd_mdtm(self,response):
code=response.headers['Status'][:3]
if code=='200':
mtime=marshal.loads(response.content)[stat.ST_MTIME]
mtime=time.gmtime(mtime)
return self.make_response('213 %4d%02d%02d%02d%02d%02d' % (
mtime[0],
mtime[1],
mtime[2],
mtime[3],
mtime[4],
mtime[5]
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file modification time.')
def cmd_size (self, line):
'return size of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_size))
def _cmd_size(self,response):
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('213 %d'%
marshal.loads(response.content)[stat.ST_SIZE])
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file size.')
self.client_dc.close_when_done()
def cmd_retr(self,line):
if len(line) < 2:
self.command_not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPget')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe,
self._cmd_retr, (line[1],)))
def _cmd_retr(self,file,response):
code=response.headers['Status'][:3]
if code=='200':
self.make_xmit_channel()
self.client_dc.push(response.content)
self.client_dc.close_when_done()
return self.make_response(
"150 Opening %s mode data connection for file '%s'" % (
self.type_map[self.current_mode],
file
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error opening file.')
def cmd_stor (self, line, mode='wb'):
'store a file'
if len (line) < 2:
self.command_not_understood (string.join (line))
return
elif self.restart_position:
restart_position = 0
self.respond ('553 restart on STOR not yet supported')
return
# XXX Check for possible problems first? Like authorization...
# But how? Once we agree to receive the file, can we still
# bail later?
fd=ContentReceiver(self._do_cmd_stor,
(self._join_paths(self.path,line[1]),))
self.respond (
'150 Opening %s connection for %s' % (
self.type_map[self.current_mode],
line[1]
)
)
self.make_recv_channel (fd)
def _do_cmd_stor(self,path,data):
'callback to do the STOR, after we have the input'
env=self._get_env()
env['PATH_INFO']=path
env['REQUEST_METHOD']='PUT'
ctype=guess_type(path)[0]
if ctype is not None:
env['CONTENT_TYPE']=ctype
env['CONTENT_LENGTH']=len(data.getvalue())
outpipe=handle(self.module,env,data)
self.push_with_producer(ResponseProducer(outpipe, self._cmd_stor))
def _cmd_stor(self,response):
code=response.headers['Status'][:3]
if code in ('200','204','302'):
return self.make_response('257 STOR command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating file.')
def cmd_dele(self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_dele))
def _cmd_dele(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 DELE command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error deleting file.')
def cmd_mkd (self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,path,'manage_addFolder')
env['QUERY_STRING']='id=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mkd))
cmd_xmkd=cmd_mkd
def _cmd_mkd(self,response):
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('257 MKD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating directory.')
def cmd_rmd (self, line):
# XXX should object be checked to see if it's folderish
# before we allow it to be RMD'd?
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_rmd))
cmd_xrmd=cmd_rmd
def _cmd_rmd(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 RMD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error removing directory.')
def cmd_user (self, line):
'specify user name'
if len(line) > 1:
self.userid = line[1]
self.respond ('331 Password required.')
else:
self.command_not_understood (string.join (line))
def cmd_pass (self, line):
'specify password'
if len(line) < 2:
pw = ''
else:
pw = line[1]
self.password=pw
i=string.find(self.userid,'@')
if i ==-1:
if self.server.limiter.check_limit(self):
self.respond ('230 Login successful.')
self.authorized = 1
self.anonymous = 1
self.log ('Successful login.')
else:
self.respond('421 User limit reached. Closing connection.')
self.close_when_done()
else:
path=self.userid[i+1:]
self.userid=self.userid[:i]
self.anonymous=None
env=self._get_env()
path=self._join_paths('/',path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_pass,(path[:-15],)))
def _cmd_pass(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
if not self.server.limiter.check_limit(self):
self.close_when_done()
return self.make_response('421 User limit reached. Closing connection.')
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('530 Unauthorized.')
self.path=path or '/'
self.authorized = 1
if self.userid=='anonymous':
self.anonymous=1
self.log('Successful login.')
return self.make_response('230 Login successful.')
else:
return self.make_response('530 Unauthorized.')
class ZResponseReceiver:
"""Given an output pipe reads response and parses it.
After a call to ready returns true, you can read
the headers as a dictiony and the content as a string."""
def __init__(self,pipe):
self.pipe=pipe
self.data=''
self.headers={}
self.content=''
def ready(self):
if self.pipe is None:
return 1
if self.pipe.ready():
data=self.pipe.read()
if data:
self.data=self.data+data
else:
self.parse()
return 1
def parse(self):
headers,html=string.split(self.data,'\n\n',1)
self.data=''
for header in string.split(headers,'\n'):
k,v=string.split(header,': ',1)
self.headers[k]=v
self.content=html
self.pipe=None
class ResponseProducer:
"Allows responses which need to make Zope requests first."
def __init__(self,pipe,callback,args=None):
self.response=ZResponseReceiver(pipe)
self.callback=callback
self.args=args or ()
self.done=None
def ready(self):
if self.response is not None:
return self.response.ready()
else:
return 1
def more(self):
if not self.done:
if not self.response.ready():
raise NotReady()
self.done=1
r=self.response
c=self.callback
args=self.args+(r,)
self.response=None
self.callback=None
self.args=None
return apply(c,args)
else:
return ''
class ContentReceiver:
"Write-only file object used to receive data from FTP"
def __init__(self,callback,args=None):
self.data=StringIO()
self.callback=callback
self.args=args or ()
def write(self,data):
self.data.write(data)
def close(self):
self.data.seek(0)
args=self.args+(self.data,)
c=self.callback
self.callback=None
self.args=None
apply(c,args)
class FTPLimiter:
"""Rudimentary FTP limits. Helps prevent denial of service
attacks. It works by limiting the number of simultaneous
connections by userid. There are three limits, one for anonymous
connections, and one for authenticated logins. The total number
of simultaneous anonymous logins my be less than or equal to the
anonymous limit. Each authenticated user can have up to the user
limit number of simultaneous connections. The total limit is the
maximum number of simultaneous connections of any sort. Do *not*
set the total limit lower than or equal to the anonymous limit."""
def __init__(self,anon_limit=10,user_limit=4,total_limit=25):
self.anon_limit=anon_limit
self.user_limit=user_limit
self.total_limit=total_limit
def check_limit(self,channel):
"""Check to see if the user has exhausted their limit or not.
Check for existing channels with the same userid and the same
ftp server."""
total=0
class_total=0
if channel.anonymous:
for existing_channel in asyncore.socket_map.keys():
if (hasattr(existing_channel,'server') and
existing_channel.server is channel.server):
total=total+1
if existing_channel.anonymous:
class_total=class_total+1
if class_total > self.anon_limit:
return None
else:
for existing_channel in asyncore.socket_map.keys():
if (hasattr(existing_channel,'server') and
existing_channel.server is channel.server):
total=total+1
if channel.userid==existing_channel.userid:
class_total=class_total+1
if class_total > self.user_limit:
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)
def __init__(self,module,*args,**kw):
apply(ftp_server.__init__, (self, None) + args, kw)
self.module=module
def handle_accept (self):
conn, addr = self.accept()
self.total_sessions.increment()
print 'Incoming connection from %s:%d' % (addr[0], addr[1])
self.ftp_channel_class (self, conn, addr, self.module)
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
\ No newline at end of file
#!/usr/local/bin/python1.5.1
"""ZServer start script
"""Sample ZServer start script"""
This start script configures ZServer.
# configuration variables
XXX comment this script much more to explain
what the different config option are.
"""
# Zope configuration
#
SOFTWARE_HOME='d:\\program files\\1.10.2'
import sys,os
sys.path.insert(0,os.path.join(SOFTWARE_HOME,'lib','python'))
# ZServer configuration
#
IP_ADDRESS=''
HOST='localhost'
DNS_IP='127.0.0.1'
DNS_IP='205.240.25.3'
HTTP_PORT=9673
FTP_PORT=8021
PCGI_PORT=88889
PID_FILE='Zope.pid'
PID_FILE=os.path.join(SOFTWARE_HOME,'var','ZServer.pid')
LOG_FILE=os.path.join(SOFTWARE_HOME,'var','ZServer.log')
MODULE='Main'
LOG_FILE='ZServer.log'
from medusa import resolver,logger,http_server,asyncore
import zope_handler
import ZServerFTP
import ZServerPCGI
from medusa import resolver,logger,http_server,asyncore
from HTTPServer import zhttp_server, zhttp_handler
from PCGIServer import PCGIServer
from FTPServer import FTPServer
rs = resolver.caching_resolver(DNS_IP)
lg = logger.file_logger(LOG_FILE)
hs = http_server.http_server(
hs = zhttp_server(
ip=IP_ADDRESS,
port=HTTP_PORT,
resolver=rs,
logger_object=lg)
zh = zope_handler.zope_handler(MODULE,'')
zh = zhttp_handler(MODULE,'')
hs.install_handler(zh)
zftp = ZServerFTP.FTPServer(
zpcgi = PCGIServer(
module=MODULE,
hostname=HOST,
port=FTP_PORT,
ip=IP_ADDRESS,
port=PCGI_PORT,
pid_file=PID_FILE,
resolver=rs,
logger_object=lg)
zpcgi = ZServerPCGI.PCGIServer(
zftp = FTPServer(
module=MODULE,
ip=IP_ADDRESS,
port=PCGI_PORT,
pid_file=PID_FILE,
hostname=HOST,
port=FTP_PORT,
resolver=rs,
logger_object=lg)
asyncore.loop()
......@@ -6,6 +6,52 @@ ZServer Changes
Releases
ZServer 1.0b1
New Features
Major code reorganization. Major architecture changes. Future
producers are gone. Use of special request and response objects
to do most of the heavy lifting.
ZServer can now handle multiple HTTP requests per connection.
Now using (almost) unmodified Medusa sources.
Added environment variable overriding to HTTPServer.py
Bugs Fixed
Using PCGI under IIS, now PATH_INFO begins with a /
Fixed FTP passive mode response message IP address.
Known Bugs
PCGI under IIS 4.0 is broken with respect to file uploads, and binary
data in general. IIS seems to convert \n to \r\n in binary data :-(
The latest medusa select_trigger.py doesn't seem to work under NT.
TODO
stress test multi-threaded channel push. (done)
test http 1.1 pipelined requests. (done)
iron out additional http 1.1 issues... (hmm)
test ftp with emacs. (done)
remove cruft.
fix medusa select_trigger under win32.
re-write Zope installers to install a ZServer start script.
get pcgi working with IIS and apache under win32, or at least get
it working under apache 1.3.6 on NT
ZServer 1.0a2
New Features
......@@ -23,7 +69,7 @@ ZServer Changes
and defer authentication until you cd to a directory where you
are defined.
Bug Fixed
Bugs Fixed
Data was being dropped from POST requests because of a bug in
the sized input collection of asynchat.
......
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
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 regsub import gsub
from base64 import encodestring
import string
class FTPRequest(HTTPRequest):
def __init__(self, path, command, channel, response, stdin=None):
if stdin is None:
stdin=StringIO()
env=self._get_env(path, command, channel, stdin)
HTTPRequest.__init__(self, stdin, env, response, clean=1)
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' % gsub('\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=='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)
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
Response class for the FTP Server.
"""
from ZPublisher.HTTPResponse import HTTPResponse
from PubCore.ZEvent import Wakeup
from cStringIO import StringIO
import marshal
class FTPResponse(HTTPResponse):
"""
Response to an FTP command
"""
def _finish(self):
self.stdout.finish(self)
def _marshalledBody(self):
return marshal.loads(self.body)
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
def write(self, text):
pass
def close(self):
pass
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,))
# is this necessary to break cycles?
self._callback=None
self._response=None
self._args=None
return result
def make_response(callback,*args):
# XXX should this be the FTPResponse constructor instead?
return FTPResponse(stdout=CallbackPipe(callback, args), stderr=StringIO())
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
ZServer HTTPResponse
The HTTPResponse class takes care of server headers, response munging
and logging duties.
"""
import time, regex, string
from cStringIO import StringIO
from ZPublisher.HTTPResponse import HTTPResponse, end_of_header_search
from medusa.http_date import build_http_date
from PubCore.ZEvent import Wakeup
from medusa.producers import hooked_producer
from medusa import http_server
from Producers import ShutdownProducer, LoggingProducer, CallbackProducer
__version__='1.0b1'
__zope_version__='1.11a1' # XXX retrieve this somehow
# XXX there should be a method somewhere for this
class ZServerHTTPResponse(HTTPResponse):
"Used to push data into a channel's producer fifo"
http_chunk=0
http_chunk_size=1024
def __str__(self,
html_search=regex.compile('<html>',regex.casefold).search,
):
if self._wrote: return '' # Streaming output was used.
headers=self.headers
body=self.body
if body:
isHTML=self.isHTML(body)
if not headers.has_key('content-type'):
if isHTML:
c='text/html'
else:
c='text/plain'
self.setHeader('content-type',c)
else:
isHTML = headers['content-type']=='text/html'
if isHTML and end_of_header_search(self.body) < 0:
lhtml=html_search(body)
if lhtml >= 0:
lhtml=lhtml+6
body='%s<head></head>\n%s' % (body[:lhtml],body[lhtml:])
else:
body='<html><head></head>\n' + body
self.setBody(body)
body=self.body
if not headers.has_key('content-type') and self.status == 200:
self.setStatus('nocontent')
if not headers.has_key('content-length'):
self.setHeader('content-length',len(body))
headersl=[]
append=headersl.append
status=headers.get('status', '200 OK')
# status header must come first.
append("HTTP/%s: %s" % (self._http_version, status))
if headers.has_key('status'):
del headers['status']
# add zserver headers
append('Server: Zope/%s ZServer/%s' % (__zope_version__, __version__))
append('Date: %s' % build_http_date(time.time()))
append('X-Powered-By: Zope (www.zope.org), Python (www.python.org)')
chunk=0
if self._http_version=='1.0':
if self._http_connection=='keep alive':
if self.headers.has_key('content-length'):
self.setHeader('Connection','close')
else:
self.setHeader('Connection','Keep-Alive')
else:
self.setHeader('Connection','close')
elif self._http_version=='1.1':
if self._http_connection=='close':
self.setHeader('Connection','close')
elif not self.headers.has_key('content-length'):
if self.headers.has_key('transfer-encoding'):
if self.headers['transfer-encoding'] != 'chunked':
self.setHeader('Connection','close')
else:
chunk=1
elif self.http_chunk:
self.setHeader('Transfer-Encoding','chunked')
chunk=1
else:
self.setHeader('Connection','close')
if chunk:
chunked_body=''
while body:
chunk=body[:self.http_chunk_size]
body=body[self.http_chunk_size:]
chunked_body='%s%x\r\n%s\r\n' % (chunked_body, len(chunk), chunk)
chunked_body='%s0\r\n\r\n' % chunked_body
body=chunked_body
for key, val in headers.items():
if string.lower(key)==key:
# only change non-literal header names
key="%s%s" % (string.upper(key[:1]), key[1:])
start=0
l=string.find(key,'-',start)
while l >= start:
key="%s-%s%s" % (key[:l],string.upper(key[l+1:l+2]),key[l+2:])
start=l+1
l=string.find(key,'-',start)
append("%s: %s" % (key, val))
if self.cookies:
headersl=headersl+self._cookie_list()
headersl[len(headersl):]=[self.accumulated_headers, body]
return string.join(headersl,'\r\n')
# XXX add server headers, etc to write()
def _finish(self):
self.stdout.finish(self)
self.stdout.close()
self.stdout=None # need to break cycle?
self._request=None
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):
self._bytes=self._bytes + len(text)
self._channel.push(text,0)
Wakeup()
def close(self):
self._channel.push(LoggingProducer(self._request, self._bytes), 0)
self._channel.push(CallbackProducer(self._channel.done))
if self._shutdown:
self._channel.push(ShutdownProducer(), 0)
elif self._close:
self._channel.push(None, 0)
Wakeup()
self._channel=None #need to break cycles?
self._request=None
def finish(self, request):
if request.headers.get('bobo-exception-type', '') == \
'exceptions.SystemExit':
self._shutdown=1
if request.headers.get('connection','')=='close':
self._close=1
self._request.reply_code=request.status
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
response._http_connection=string.lower(
http_server.get_header(http_server.CONNECTION, request.header))
return response
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
......@@ -17,12 +19,13 @@
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
......@@ -73,27 +76,6 @@
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
......@@ -102,27 +84,40 @@
##############################################################################
"""
Zope Medusa Handler
Medusa HTTP server for Zope
Uses a new threaded architecture.
Works with http_server.py
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 regex
import string
import os
import types
from cStringIO import StringIO
import thread
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, http_channel
from medusa import counter, producers, asyncore
from medusa.default_handler import split_path, unquote, get_header
from medusa.producers import NotReady
CONTENT_LENGTH = regex.compile('Content-Length: \([0-9]+\)',regex.casefold)
CONNECTION = regex.compile ('Connection: \(.*\)', regex.casefold)
......@@ -135,20 +130,24 @@ header2env={'content-length' : 'CONTENT_LENGTH',
}
class zope_handler:
"publishes a module with ZPublisher"
class zhttp_handler:
"A medusa style handler for zhttp_server"
# XXX add code to allow env overriding
def __init__ (self, module, uri_base=None):
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
......@@ -166,7 +165,7 @@ class zope_handler:
uri_regex='%s.*' % self.uri_base
self.uri_regex = regex.compile(uri_regex)
def match (self, request):
def match(self, request):
uri = request.uri
if self.uri_regex.match(uri) == len(uri):
return 1
......@@ -204,11 +203,11 @@ class zope_handler:
# ZPublisher doesn't want the leading '?'
query = query[1:]
env = {}
env['REQUEST_METHOD'] = string.upper(request.command)
env['SERVER_PORT'] = str(request.channel.server.port)
env['SERVER_NAME'] = request.channel.server.server_name
env['SERVER_SOFTWARE'] = request['Server']
env['SERVER_PROTOCOL']='HTTP/1.0' # should this be 1.1?
env['REQUEST_METHOD']=string.upper(request.command)
env['SERVER_PORT']=str(request.channel.server.port)
env['SERVER_NAME']=request.channel.server.server_name
env['SERVER_SOFTWARE']=request['Server']
env['SERVER_PROTOCOL']=request.version
if self.uri_base=='/':
env['SCRIPT_NAME']=''
env['PATH_INFO']=path
......@@ -243,62 +242,20 @@ class zope_handler:
string.join(string.split(key, "-"), "_"))
if value and not env.has_key(key):
env[key]=value
for key, value in self.env_override.items():
env[key]=value
return env
def continue_request(self,sin,request):
def continue_request(self, sin, request):
"continue handling request now that we have the stdin"
outpipe=handle(self.module_name,self.get_environment(request),sin)
# now comes the hairy stuff. adapted from http_server
#
connection = string.lower(get_header(CONNECTION,request.header))
close_it = 0
wrap_in_chunking = 0
if request.version == '1.0':
if connection == 'keep-alive':
if not request.has_key ('Content-Length'):
close_it = 1
else:
request['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif request.version == '1.1':
if connection == 'close':
close_it = 1
elif not request.has_key ('Content-Length'):
if request.has_key ('Transfer-Encoding'):
if not request['Transfer-Encoding'] == 'chunked':
close_it = 1
elif request.use_chunked:
request['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
else:
close_it = 1
if close_it:
request['Connection'] = 'close'
outgoing_producer = header_scanning_producer(request,outpipe)
# apply a few final transformations to the output
request.channel.push_with_producer (
# hooking lets us log the number of bytes sent
producers.hooked_producer (
outgoing_producer,
request.log
)
)
request.channel.current_request = None
if close_it:
request.channel.close_when_done()
def status (self):
env=self.get_environment(request)
zresponse=make_response(request,env)
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>
......@@ -325,48 +282,39 @@ class zope_handler:
self.continue_request(d,r)
class header_scanning_producer:
"""This weird producer patches together
medusa's idea of http headers with ZPublisher's
"""
def __init__(self,request,pipe):
self.request=request
self.pipe=pipe
self.done=None
self.buffer=''
self.exit=None
def more(self):
if self.buffer:
b=self.buffer
self.buffer=''
return b
data=self.pipe.read()
if data is None:
raise NotReady()
if data=='' and self.exit:
asyncore.close_all()
return data
def ready(self):
if self.done:
return self.pipe.ready()
elif self.pipe.ready():
self.buffer=self.buffer+self.pipe.read()
if string.find(self.buffer,"\n\n") != -1:
[headers,html]=string.split(self.buffer,"\n\n",1)
headers=string.split(headers,"\n")
for line in headers:
[header, header_value]=string.split(line,": ",1)
if header=="Status":
[code,message]=string.split(header_value," ",1)
self.request.reply_code=string.atoi(code)
elif header=="Bobo-Exception-Type" and \
header_value=="exceptions.SystemExit":
self.exit=1
else:
self.request[header]=header_value
self.buffer=self.request.build_reply_header()+html
del self.request
self.done=1
class zhttp_channel(http_channel):
"http channel"
def __init__(self, server, conn, addr):
http_channel.__init__(self, server, conn, addr)
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
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
module_name, request, response=self.queue[0]
self.queue=self.queue[1:]
handle(module_name, request, response)
def done(self):
"Called when a pushing request is finished"
self.working=0
self.work()
class zhttp_server(http_server):
"http server"
channel_class = zhttp_channel
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
......@@ -17,12 +19,13 @@
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
......@@ -73,27 +76,6 @@
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
......@@ -101,7 +83,8 @@
#
##############################################################################
"""First cut at a Medusa PCGI server.
"""
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
......@@ -119,15 +102,22 @@ Note that ZServer can operate multiple PCGI servers.
from medusa import asynchat, asyncore, logger
from medusa.counter import counter
from medusa.producers import NotReady, hooked_producer
from medusa.http_server import compute_timezone_for_log
from PubCore import handle
from PubCore.ZEvent import Wakeup
from ZPublisher.HTTPResponse import HTTPResponse
from ZPublisher.HTTPRequest import HTTPRequest
from Producers import ShutdownProducer, LoggingProducer
from cStringIO import StringIO
from tempfile import TemporaryFile
import socket
import string
import os
import time
tz_for_log=compute_timezone_for_log()
class PCGIChannel(asynchat.async_chat):
"""Processes a PCGI request by collecting the env and stdin and
......@@ -138,10 +128,10 @@ class PCGIChannel(asynchat.async_chat):
self.server = server
self.addr = addr
asynchat.async_chat.__init__ (self, sock)
self.env={}
self.data=StringIO()
self.set_terminator(10)
self.size=None
self.env={}
self.done=None
def found_terminator(self):
......@@ -150,6 +140,7 @@ class PCGIChannel(asynchat.async_chat):
# and prepare to read env or stdin
self.data.seek(0)
self.size=string.atoi(self.data.read())
print "got size", self.size
self.set_terminator(self.size)
if self.size==0:
self.set_terminator('\r\n')
......@@ -179,13 +170,14 @@ class PCGIChannel(asynchat.async_chat):
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.env['PATH_INFO'] = '/' + string.join(path[len(script):],'/')
print "REQUEST", self.env['REQUEST_METHOD']
self.data=StringIO()
# now read the next size header
self.set_terminator(10)
else:
# we're done, we've got both env and stdin
print "got stdin", len(self.data.getvalue())
self.set_terminator('\r\n')
self.data.seek(0)
self.send_response()
......@@ -195,11 +187,9 @@ class PCGIChannel(asynchat.async_chat):
# and requesting a callback of self.log with the module
# name and PATH_INFO as an argument.
self.done=1
outpipe=handle(self.server.module, self.env, self.data)
self.push_with_producer(
hooked_producer(PCGIProducer(outpipe), self.log_request)
)
self.close_when_done()
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)
......@@ -209,20 +199,33 @@ class PCGIChannel(asynchat.async_chat):
return 1
def log_request(self, bytes):
# XXX need to add reply code logging
if self.env.has_key('PATH_INFO'):
path='%s/%s' % (self.server.module, self.env['PATH_INFO'])
path='%s%s' % (self.server.module, 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"
self.server.logger.log (
self.addr[0],
'%d - - [time goes here] "%s" %d' % (
self.addr[1], path, bytes
'%d - - [%s] "%s %s" %d' % (
self.addr[1],
time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.gmtime(time.time())
) + tz_for_log,
method, path, bytes
)
)
def writable(self):
return len(self.ac_out_buffer) or self.producer_fifo.ready() \
or (not self.connected)
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()
class PCGIServer(asyncore.dispatcher):
......@@ -271,7 +274,7 @@ class PCGIServer(asyncore.dispatcher):
# write pid file
try:
f = open(self.pid_file, 'wb')
f = open(self.pid_file, 'w')
f.write(str(os.getpid()))
f.close()
except IOError:
......@@ -329,42 +332,45 @@ class PCGIServer(asyncore.dispatcher):
return 0
class PCGIProducer:
"""Producer wrapper over output pipe
class PCGIResponse(HTTPResponse):
output format is in PCGI format: 10 digits indicating len of
STDOUT followed by STDOUT.
def _finish(self):
self.stdout.finish(self)
self.stdout.close()
Note: no STDERR is returned.
Note: streaming is not supported, since PCGI requires the length
of the STDOUT be known before it is sent.
class PCGIPipe:
"""
Formats a HTTP response in PCGI format
def __init__(self,pipe):
self.pipe=pipe
self.buffer=StringIO()
self.buffer.write('0'*10) #this will hold length header
self.chunk_size=512
self.done=None
10 digits indicating len of STDOUT
STDOUT
10 digits indicating len of STDERR
STDERR
def ready(self):
if self.done:
return 1
data=self.pipe.read()
if data is not None:
self.buffer.write(data)
if data=='':
self.buffer.write('0'*10) #write stderr header
self.buffer.seek(0)
size=len(self.buffer.getvalue())-20
self.buffer.write('%010d' % size)
self.buffer.seek(0)
self.done=1
return 1
def more(self):
if not self.done:
raise NotReady
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):
data=self._data.getvalue()
l=len(data)
self._channel.push('%010d%s%010d' % (l, data, 0), 0)
self._channel.push(LoggingProducer(self._channel, l, 'log_request'), 0)
if self._shutdown:
self._channel.push(ShutdownProducer(), 0)
else:
return self.buffer.read(self.chunk_size)
self._channel.push(None, 0)
Wakeup()
self._channel=None
def finish(self,request):
if request.headers.get('bobo-exception-type','') == \
'exceptions.SystemExit':
self._shutdown=1
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
ZServer pipe utils. These producers basically function as callbacks.
"""
from medusa 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)
return ''
class CallbackProducer:
"Performs a callback in the channel's thread"
def __init__(self, callback):
self.callback=callback
def more(self):
self.callback()
return ''
\ No newline at end of file
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.6
# ---------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Any use, including use of the Zope software to operate a website,
# must either comply with the terms described below under
# "Attribution" or alternatively secure a separate license from
# Digital Creations. Digital Creations will not unreasonably
# deny such a separate license in the event that the request
# explains in detail a valid reason for withholding attribution.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web
# site ("the web site") must provide attribution by placing
# the accompanying "button" on the website's main entry
# point. By default, the button links to a "credits page"
# on the Digital Creations' web site. The "credits page" may
# be copied to "the web site" in order to add other credits,
# or keep users "on site". In that case, the "button" link
# may be updated to point to the "on site" "credits page".
# In cases where this placement of attribution is not
# feasible, a separate arrangment must be concluded with
# Digital Creations. Those using the software for purposes
# other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment. Where
# attribution is not possible, or is considered to be
# onerous for some other reason, a request should be made to
# Digital Creations to waive this requirement in writing.
# As stated above, for valid requests, Digital Creations
# will not unreasonably deny such requests.
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""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.
Optionally, users can login with a special username which indicates
where they are defined. Their login will then be authenticated in
the indicated directory, and they will not be considered anonymous.
The form of the name is '<username>@<path>' where path takes the form
'<folder id>[/<folder id>...]' For example: 'amos@Foo/Bar' This will
authenticate the user 'amos' in the directory '/Foo/Bar'. In addition
the user's FTP session will be rooted in the authenticated directory,
i.e. they will not be able to cd out of the directory.
The main reason to use the rooted FTP login, is to allow non-anonymous
logins. This may be handy, if for example, you disallow anonymous logins,
or if you set the limit for simultaneous anonymous logins very low.
"""
from PubCore import handle
from medusa.ftp_server import ftp_channel, ftp_server
from medusa import asyncore, asynchat, filesys
from medusa.producers import NotReady
from cStringIO import StringIO
import string
import os
from regsub import gsub
from base64 import encodestring
from mimetypes import guess_type
import marshal
import stat
import time
class zope_ftp_channel(ftp_channel):
"Passes its commands to Zope, not a filesystem"
read_only=0
anonymous=1
def __init__ (self, server, conn, addr, module):
ftp_channel.__init__(self,server,conn,addr)
self.module=module
self.userid=''
self.password=''
self.path='/'
def _get_env(self):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % self.module
env['PATH_INFO']=self.path
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=self.server.SERVER_IDENT
if self.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % gsub('\012','',
encodestring('%s:%s' % (self.userid,self.password)))
env['BOBO_DEBUG_MODE']='1'
env['SERVER_NAME']=self.server.hostname
env['SERVER_PORT']=str(self.server.port)
env['REMOTE_ADDR']=self.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# XXX etcetera -- probably set many of these at the start, rather
# than for each request...
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=string.replace(path,os.sep,'/')
return path
def make_response(self,resp):
self.log ('==> %s' % resp)
return resp + '\r\n'
# Overriden async_chat methods
writable=asynchat.async_chat.writable_future
# Overriden ftp_channel methods
def cmd_nlst (self, line):
'give name list of files in directory'
self.push_with_producer(self.get_dir_list (line, 0))
def cmd_list (self, line):
'give list files in a directory'
# handles files as well as directories.
# XXX also should maybe handle globbing, yuck.
self.push_with_producer(self.get_dir_list (line, 1))
def get_dir_list (self, line, long=0):
# we need to scan the command line for arguments to '/bin/ls'...
# XXX clean this up, maybe with getopts
if len(line) > 1:
args = string.split(line[1])
else:
args =[]
path_args = []
for arg in args:
if arg[0] != '-':
path_args.append (arg)
else:
if 'l' in arg:
long=1
if len(path_args) < 1:
dir = '.'
else:
dir = path_args[0]
return self.listdir (dir, long)
def listdir (self, path, long=0):
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_FTPlist')
outpipe=handle(self.module,env,StringIO())
return ResponseProducer(outpipe,self._do_listdir,(long,))
def _do_listdir(self,long,response):
code=response.headers['Status'][:3]
if code=='200':
if self.anonymous and not self.userid=='anonymous':
self.anonymous=None
dir_list=''
file_infos=marshal.loads(response.content)
if type(file_infos[0])==type(''):
file_infos=(file_infos,)
if long:
for id, stat_info in file_infos:
dir_list=dir_list+filesys.unix_longify(id,stat_info)+'\r\n'
else:
for id, stat_info in file_infos:
dir_list=dir_list+id+'\r\n'
self.make_xmit_channel()
self.client_dc.push(dir_list)
self.client_dc.close_when_done()
return self.make_response(
'150 Opening %s mode data connection for file list' % (
self.type_map[self.current_mode]
)
)
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Could not list directory.')
def cmd_cwd (self, line):
'change working directory'
# try to call manage_FTPlist on the path
env=self._get_env()
path=line[1]
path=self._join_paths(self.path,path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_cwd,(path[:-15],)))
def _cmd_cwd(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('550 No such directory.')
self.path=path or '/'
return self.make_response('250 CWD command successful.')
if self.anonymous and not self.userid=='anonymous':
self.anonymous=None
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 No such directory.')
def cmd_cdup (self, line):
'change to parent of current working directory'
self.cmd_cwd((None,'..'))
def cmd_pwd (self, line):
'print the current working directory'
self.respond (
'257 "%s" is the current directory.' % (
self.path
)
)
cmd_xpwd=cmd_pwd
def cmd_mdtm (self, line):
'show last modification time of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mdtm))
def _cmd_mdtm(self,response):
code=response.headers['Status'][:3]
if code=='200':
mtime=marshal.loads(response.content)[stat.ST_MTIME]
mtime=time.gmtime(mtime)
return self.make_response('213 %4d%02d%02d%02d%02d%02d' % (
mtime[0],
mtime[1],
mtime[2],
mtime[3],
mtime[4],
mtime[5]
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file modification time.')
def cmd_size (self, line):
'return size of file'
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPstat')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_size))
def _cmd_size(self,response):
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('213 %d'%
marshal.loads(response.content)[stat.ST_SIZE])
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error getting file size.')
self.client_dc.close_when_done()
def cmd_retr(self,line):
if len(line) < 2:
self.command_not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,line[1],'manage_FTPget')
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe,
self._cmd_retr, (line[1],)))
def _cmd_retr(self,file,response):
code=response.headers['Status'][:3]
if code=='200':
self.make_xmit_channel()
self.client_dc.push(response.content)
self.client_dc.close_when_done()
return self.make_response(
"150 Opening %s mode data connection for file '%s'" % (
self.type_map[self.current_mode],
file
))
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error opening file.')
def cmd_stor (self, line, mode='wb'):
'store a file'
if len (line) < 2:
self.command_not_understood (string.join (line))
return
elif self.restart_position:
restart_position = 0
self.respond ('553 restart on STOR not yet supported')
return
# XXX Check for possible problems first? Like authorization...
# But how? Once we agree to receive the file, can we still
# bail later?
fd=ContentReceiver(self._do_cmd_stor,
(self._join_paths(self.path,line[1]),))
self.respond (
'150 Opening %s connection for %s' % (
self.type_map[self.current_mode],
line[1]
)
)
self.make_recv_channel (fd)
def _do_cmd_stor(self,path,data):
'callback to do the STOR, after we have the input'
env=self._get_env()
env['PATH_INFO']=path
env['REQUEST_METHOD']='PUT'
ctype=guess_type(path)[0]
if ctype is not None:
env['CONTENT_TYPE']=ctype
env['CONTENT_LENGTH']=len(data.getvalue())
outpipe=handle(self.module,env,data)
self.push_with_producer(ResponseProducer(outpipe, self._cmd_stor))
def _cmd_stor(self,response):
code=response.headers['Status'][:3]
if code in ('200','204','302'):
return self.make_response('257 STOR command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating file.')
def cmd_dele(self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_dele))
def _cmd_dele(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 DELE command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error deleting file.')
def cmd_mkd (self, line):
if len (line) != 2:
self.command.not_understood (string.join (line))
return
env=self._get_env()
path,id=os.path.split(line[1])
env['PATH_INFO']=self._join_paths(self.path,path,'manage_addFolder')
env['QUERY_STRING']='id=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_mkd))
cmd_xmkd=cmd_mkd
def _cmd_mkd(self,response):
code=response.headers['Status'][:3]
if code=='200':
return self.make_response('257 MKD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error creating directory.')
def cmd_rmd (self, line):
# XXX should object be checked to see if it's folderish
# before we allow it to be RMD'd?
if len (line) != 2:
self.command.not_understood (string.join (line))
return
path,id=os.path.split(line[1])
env=self._get_env()
env['PATH_INFO']=self._join_paths(self.path,path,'manage_delObjects')
env['QUERY_STRING']='ids=%s' % id
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer(outpipe, self._cmd_rmd))
cmd_xrmd=cmd_rmd
def _cmd_rmd(self,response):
code=response.headers['Status'][:3]
if code=='200' and string.find(response.content,'Not Deletable')==-1:
return self.make_response('250 RMD command successful.')
elif code=='401':
return self.make_response('530 Unauthorized.')
else:
return self.make_response('550 Error removing directory.')
def cmd_user (self, line):
'specify user name'
if len(line) > 1:
self.userid = line[1]
self.respond ('331 Password required.')
else:
self.command_not_understood (string.join (line))
def cmd_pass (self, line):
'specify password'
if len(line) < 2:
pw = ''
else:
pw = line[1]
self.password=pw
i=string.find(self.userid,'@')
if i ==-1:
if self.server.limiter.check_limit(self):
self.respond ('230 Login successful.')
self.authorized = 1
self.anonymous = 1
self.log ('Successful login.')
else:
self.respond('421 User limit reached. Closing connection.')
self.close_when_done()
else:
path=self.userid[i+1:]
self.userid=self.userid[:i]
self.anonymous=None
env=self._get_env()
path=self._join_paths('/',path,'manage_FTPlist')
env['PATH_INFO']=path
outpipe=handle(self.module,env,StringIO())
self.push_with_producer(ResponseProducer
(outpipe,self._cmd_pass,(path[:-15],)))
def _cmd_pass(self,path,response):
code=response.headers['Status'][:3]
if code=='200':
if not self.server.limiter.check_limit(self):
self.close_when_done()
return self.make_response('421 User limit reached. Closing connection.')
listing=marshal.loads(response.content)
# check to see if we are cding to a non-foldoid object
if type(listing[0])==type(''):
return self.make_response('530 Unauthorized.')
self.path=path or '/'
self.authorized = 1
if self.userid=='anonymous':
self.anonymous=1
self.log('Successful login.')
return self.make_response('230 Login successful.')
else:
return self.make_response('530 Unauthorized.')
class ZResponseReceiver:
"""Given an output pipe reads response and parses it.
After a call to ready returns true, you can read
the headers as a dictiony and the content as a string."""
def __init__(self,pipe):
self.pipe=pipe
self.data=''
self.headers={}
self.content=''
def ready(self):
if self.pipe is None:
return 1
if self.pipe.ready():
data=self.pipe.read()
if data:
self.data=self.data+data
else:
self.parse()
return 1
def parse(self):
headers,html=string.split(self.data,'\n\n',1)
self.data=''
for header in string.split(headers,'\n'):
k,v=string.split(header,': ',1)
self.headers[k]=v
self.content=html
self.pipe=None
class ResponseProducer:
"Allows responses which need to make Zope requests first."
def __init__(self,pipe,callback,args=None):
self.response=ZResponseReceiver(pipe)
self.callback=callback
self.args=args or ()
self.done=None
def ready(self):
if self.response is not None:
return self.response.ready()
else:
return 1
def more(self):
if not self.done:
if not self.response.ready():
raise NotReady()
self.done=1
r=self.response
c=self.callback
args=self.args+(r,)
self.response=None
self.callback=None
self.args=None
return apply(c,args)
else:
return ''
class ContentReceiver:
"Write-only file object used to receive data from FTP"
def __init__(self,callback,args=None):
self.data=StringIO()
self.callback=callback
self.args=args or ()
def write(self,data):
self.data.write(data)
def close(self):
self.data.seek(0)
args=self.args+(self.data,)
c=self.callback
self.callback=None
self.args=None
apply(c,args)
class FTPLimiter:
"""Rudimentary FTP limits. Helps prevent denial of service
attacks. It works by limiting the number of simultaneous
connections by userid. There are three limits, one for anonymous
connections, and one for authenticated logins. The total number
of simultaneous anonymous logins my be less than or equal to the
anonymous limit. Each authenticated user can have up to the user
limit number of simultaneous connections. The total limit is the
maximum number of simultaneous connections of any sort. Do *not*
set the total limit lower than or equal to the anonymous limit."""
def __init__(self,anon_limit=10,user_limit=4,total_limit=25):
self.anon_limit=anon_limit
self.user_limit=user_limit
self.total_limit=total_limit
def check_limit(self,channel):
"""Check to see if the user has exhausted their limit or not.
Check for existing channels with the same userid and the same
ftp server."""
total=0
class_total=0
if channel.anonymous:
for existing_channel in asyncore.socket_map.keys():
if (hasattr(existing_channel,'server') and
existing_channel.server is channel.server):
total=total+1
if existing_channel.anonymous:
class_total=class_total+1
if class_total > self.anon_limit:
return None
else:
for existing_channel in asyncore.socket_map.keys():
if (hasattr(existing_channel,'server') and
existing_channel.server is channel.server):
total=total+1
if channel.userid==existing_channel.userid:
class_total=class_total+1
if class_total > self.user_limit:
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)
def __init__(self,module,*args,**kw):
apply(ftp_server.__init__, (self, None) + args, kw)
self.module=module
def handle_accept (self):
conn, addr = self.accept()
self.total_sessions.increment()
print 'Incoming connection from %s:%d' % (addr[0], addr[1])
self.ftp_channel_class (self, conn, addr, self.module)
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
\ No newline at end of file
#!/usr/local/bin/python1.5.1
"""ZServer start script
"""Sample ZServer start script"""
This start script configures ZServer.
# configuration variables
XXX comment this script much more to explain
what the different config option are.
"""
# Zope configuration
#
SOFTWARE_HOME='d:\\program files\\1.10.2'
import sys,os
sys.path.insert(0,os.path.join(SOFTWARE_HOME,'lib','python'))
# ZServer configuration
#
IP_ADDRESS=''
HOST='localhost'
DNS_IP='127.0.0.1'
DNS_IP='205.240.25.3'
HTTP_PORT=9673
FTP_PORT=8021
PCGI_PORT=88889
PID_FILE='Zope.pid'
PID_FILE=os.path.join(SOFTWARE_HOME,'var','ZServer.pid')
LOG_FILE=os.path.join(SOFTWARE_HOME,'var','ZServer.log')
MODULE='Main'
LOG_FILE='ZServer.log'
from medusa import resolver,logger,http_server,asyncore
import zope_handler
import ZServerFTP
import ZServerPCGI
from medusa import resolver,logger,http_server,asyncore
from HTTPServer import zhttp_server, zhttp_handler
from PCGIServer import PCGIServer
from FTPServer import FTPServer
rs = resolver.caching_resolver(DNS_IP)
lg = logger.file_logger(LOG_FILE)
hs = http_server.http_server(
hs = zhttp_server(
ip=IP_ADDRESS,
port=HTTP_PORT,
resolver=rs,
logger_object=lg)
zh = zope_handler.zope_handler(MODULE,'')
zh = zhttp_handler(MODULE,'')
hs.install_handler(zh)
zftp = ZServerFTP.FTPServer(
zpcgi = PCGIServer(
module=MODULE,
hostname=HOST,
port=FTP_PORT,
ip=IP_ADDRESS,
port=PCGI_PORT,
pid_file=PID_FILE,
resolver=rs,
logger_object=lg)
zpcgi = ZServerPCGI.PCGIServer(
zftp = FTPServer(
module=MODULE,
ip=IP_ADDRESS,
port=PCGI_PORT,
pid_file=PID_FILE,
hostname=HOST,
port=FTP_PORT,
resolver=rs,
logger_object=lg)
asyncore.loop()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment