zope.server-3.8.6/0000775000175000017500000000000011701734350013045 5ustar mgmg00000000000000zope.server-3.8.6/CHANGES.txt0000664000175000017500000000741311701734341014663 0ustar mgmg00000000000000======= CHANGES ======= 3.8.6 (2012-01-07) ------------------ - On startup, HTTPServer prints a clickable URL after the hostname/port. 3.8.5 (2011-09-13) ------------------ - fixed bug: requests lasting over 15 minutes were sometimes closed prematurely. 3.8.4 (2011-06-07) ------------------ - Fix syntax error in tests on Python < 2.6. 3.8.3 (2011-05-18) ------------------ - Made ``start_response`` method of WSGI server implementation more compliant with spec: http://www.python.org/dev/peps/pep-0333/#the-start-response-callable 3.8.2 (2010-12-04) ------------------ - Corrected license version in ``zope/server/http/tests/test_wsgiserver.py``. 3.8.1 (2010-08-24) ------------------ - When the result of a WSGI application was received, ``task.write()`` was only called once to transmit the data. This prohibited the transmission of partial results. Now the WSGI server iterates through the result itself making multiple ``task.write()`` calls, which will cause partial data to be transmitted. - Created a second test case instance for the post-mortem WSGI server, so it is tested as well. - Using python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 3.8.0 (2010-08-05) ------------------ - Implemented correct server proxy behavior. The HTTP server would always add a "Server" and "Date" response header to the list of response headers regardless whether one had been set already. The HTTP 1.1 spec specifies that a proxy server must not modify the "Server" and "Date" header but add a "Via" header instead. 3.7.0 (2010-08-01) ------------------ - Implemented proxy support. Proxy requests contain a full URIs and the request parser used to throw that information away. Using ``urlparse.urlsplit()``, all pieces of the URL are recorded. - The proxy acheme and netloc/hostname are exposed in the WSGI environment as ``zserver.proxy.scheme`` and ``zserver.proxy.host``. - Made tests runnable via buildout again. 3.6.2 (2010-06-11) ------------------ - The log message "Exception during task" is no longer logged to the root logger but to zope.server.taskthreads. 3.6.1 (2009-10-07) ------------------ - Made tests pass with current zope.publisher which restricts redirects to the current host by default. 3.6.0 (2009-05-27) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner - Removed unused dependency on zope.deprecation. - Remove old zpkg-related DEPENDENCIES.cfg file. 3.5.0 (2008-03-01) ------------------ - Improve package meta-data. - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html - Removed dependency on ZODB. 3.5.0a2 (2007-06-02) -------------------- - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.5.0a1 (2007-06-02) -------------------- - Added a factory and entry point for PasteDeploy. 3.4.3 (2008-08-18) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner 3.4.2 (2008-02-02) ------------------ - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html 3.4.1 (2007-06-02) ------------------ - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.4.0 (2007-06-02) ------------------ - Removed an unused import. Unchanged otherwise. 3.4.0a1 (2007-04-22) -------------------- - Initial release as a separate project, corresponds to zope.server from Zope 3.4.0a1 - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. zope.server-3.8.6/setup.py0000664000175000017500000000473711701734341014572 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## # This package is developed by the Zope Toolkit project, documented here: # http://docs.zope.org/zopetoolkit # When developing and releasing this package, please follow the documented # Zope Toolkit policies as described by this documentation. ############################################################################## """Setup for zope.server package """ import os from setuptools import setup, find_packages def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() tests_require = [ 'zope.testing', 'zope.i18n', 'zope.component', ] setup( name='zope.server', version='3.8.6', author='Zope Foundation and Contributors', author_email='zope-dev@zope.org', description='Zope Server (Web and FTP)', long_description=( read('README.txt') + '\n\n' + read('CHANGES.txt') ), license='ZPL 2.1', keywords=('zope3 server http ftp'), classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: Zope Public License', 'Programming Language :: Python', 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP', 'Framework :: Zope3'], url='http://pypi.python.org/pypi/zope.server', packages=find_packages('src'), package_dir = {'': 'src'}, namespace_packages=['zope',], tests_require=tests_require, install_requires=[ 'setuptools', 'zope.interface', 'zope.publisher', 'zope.security', ], extras_require=dict(test=tests_require), include_package_data=True, zip_safe=False, entry_points=""" [paste.server_runner] main = zope.server.http.wsgihttpserver:run_paste """ ) zope.server-3.8.6/COPYRIGHT.txt0000664000175000017500000000004011701734341015150 0ustar mgmg00000000000000Zope Foundation and Contributorszope.server-3.8.6/src/0000775000175000017500000000000011701734347013642 5ustar mgmg00000000000000zope.server-3.8.6/src/zope.server.egg-info/0000775000175000017500000000000011701734350017610 5ustar mgmg00000000000000zope.server-3.8.6/src/zope.server.egg-info/entry_points.txt0000664000175000017500000000012411701734346023110 0ustar mgmg00000000000000 [paste.server_runner] main = zope.server.http.wsgihttpserver:run_paste zope.server-3.8.6/src/zope.server.egg-info/namespace_packages.txt0000664000175000017500000000000511701734346024143 0ustar mgmg00000000000000zope zope.server-3.8.6/src/zope.server.egg-info/requires.txt0000664000175000017500000000014411701734346022214 0ustar mgmg00000000000000setuptools zope.interface zope.publisher zope.security [test] zope.testing zope.i18n zope.componentzope.server-3.8.6/src/zope.server.egg-info/not-zip-safe0000664000175000017500000000000111701734342022037 0ustar mgmg00000000000000 zope.server-3.8.6/src/zope.server.egg-info/PKG-INFO0000664000175000017500000001465511701734346020725 0ustar mgmg00000000000000Metadata-Version: 1.0 Name: zope.server Version: 3.8.6 Summary: Zope Server (Web and FTP) Home-page: http://pypi.python.org/pypi/zope.server Author: Zope Foundation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: This package contains generic base classes for channel-based servers, the servers themselves and helper objects, such as tasks and requests. ============ WSGI Support ============ `zope.server`'s HTTP server comes with WSGI_ support. ``zope.server.http.wsgihttpserver.WSGIHTTPServer`` can act as a WSGI gateway. There's also an entry point for PasteDeploy_ that lets you use zope.server's WSGI gateway from a configuration file, e.g.:: [server:main] use = egg:zope.server host = 127.0.0.1 port = 8080 .. _WSGI: http://www.python.org/dev/peps/pep-0333/ .. _PasteDeploy: http://pythonpaste.org/deploy/ ======= CHANGES ======= 3.8.6 (2012-01-07) ------------------ - On startup, HTTPServer prints a clickable URL after the hostname/port. 3.8.5 (2011-09-13) ------------------ - fixed bug: requests lasting over 15 minutes were sometimes closed prematurely. 3.8.4 (2011-06-07) ------------------ - Fix syntax error in tests on Python < 2.6. 3.8.3 (2011-05-18) ------------------ - Made ``start_response`` method of WSGI server implementation more compliant with spec: http://www.python.org/dev/peps/pep-0333/#the-start-response-callable 3.8.2 (2010-12-04) ------------------ - Corrected license version in ``zope/server/http/tests/test_wsgiserver.py``. 3.8.1 (2010-08-24) ------------------ - When the result of a WSGI application was received, ``task.write()`` was only called once to transmit the data. This prohibited the transmission of partial results. Now the WSGI server iterates through the result itself making multiple ``task.write()`` calls, which will cause partial data to be transmitted. - Created a second test case instance for the post-mortem WSGI server, so it is tested as well. - Using python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 3.8.0 (2010-08-05) ------------------ - Implemented correct server proxy behavior. The HTTP server would always add a "Server" and "Date" response header to the list of response headers regardless whether one had been set already. The HTTP 1.1 spec specifies that a proxy server must not modify the "Server" and "Date" header but add a "Via" header instead. 3.7.0 (2010-08-01) ------------------ - Implemented proxy support. Proxy requests contain a full URIs and the request parser used to throw that information away. Using ``urlparse.urlsplit()``, all pieces of the URL are recorded. - The proxy acheme and netloc/hostname are exposed in the WSGI environment as ``zserver.proxy.scheme`` and ``zserver.proxy.host``. - Made tests runnable via buildout again. 3.6.2 (2010-06-11) ------------------ - The log message "Exception during task" is no longer logged to the root logger but to zope.server.taskthreads. 3.6.1 (2009-10-07) ------------------ - Made tests pass with current zope.publisher which restricts redirects to the current host by default. 3.6.0 (2009-05-27) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner - Removed unused dependency on zope.deprecation. - Remove old zpkg-related DEPENDENCIES.cfg file. 3.5.0 (2008-03-01) ------------------ - Improve package meta-data. - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html - Removed dependency on ZODB. 3.5.0a2 (2007-06-02) -------------------- - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.5.0a1 (2007-06-02) -------------------- - Added a factory and entry point for PasteDeploy. 3.4.3 (2008-08-18) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner 3.4.2 (2008-02-02) ------------------ - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html 3.4.1 (2007-06-02) ------------------ - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.4.0 (2007-06-02) ------------------ - Removed an unused import. Unchanged otherwise. 3.4.0a1 (2007-04-22) -------------------- - Initial release as a separate project, corresponds to zope.server from Zope 3.4.0a1 - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. Keywords: zope3 server http ftp Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Framework :: Zope3 zope.server-3.8.6/src/zope.server.egg-info/SOURCES.txt0000664000175000017500000000542111701734346021503 0ustar mgmg00000000000000CHANGES.txt COPYRIGHT.txt LICENSE.txt README.txt bootstrap.py buildout.cfg setup.py test.ini src/zope/__init__.py src/zope.server.egg-info/PKG-INFO src/zope.server.egg-info/SOURCES.txt src/zope.server.egg-info/dependency_links.txt src/zope.server.egg-info/entry_points.txt src/zope.server.egg-info/namespace_packages.txt src/zope.server.egg-info/not-zip-safe src/zope.server.egg-info/requires.txt src/zope.server.egg-info/top_level.txt src/zope/server/__init__.py src/zope/server/adjustments.py src/zope/server/buffers.py src/zope/server/dualmodechannel.py src/zope/server/fixedstreamreceiver.py src/zope/server/maxsockets.py src/zope/server/serverbase.py src/zope/server/serverchannelbase.py src/zope/server/taskthreads.py src/zope/server/trigger.py src/zope/server/utilities.py src/zope/server/zlogintegration.py src/zope/server/ftp/README.txt src/zope/server/ftp/__init__.py src/zope/server/ftp/logger.py src/zope/server/ftp/publisher.py src/zope/server/ftp/server.py src/zope/server/ftp/tests/__init__.py src/zope/server/ftp/tests/demofs.py src/zope/server/ftp/tests/fstests.py src/zope/server/ftp/tests/test_demofs.py src/zope/server/ftp/tests/test_ftpserver.py src/zope/server/ftp/tests/test_publisher.py src/zope/server/http/__init__.py src/zope/server/http/chunking.py src/zope/server/http/commonaccesslogger.py src/zope/server/http/http_date.py src/zope/server/http/httprequestparser.py src/zope/server/http/httpserver.py src/zope/server/http/httpserverchannel.py src/zope/server/http/httptask.py src/zope/server/http/publisherhttpserver.py src/zope/server/http/wsgihttpserver.py src/zope/server/http/tests/__init__.py src/zope/server/http/tests/test_commonaccesslogger.py src/zope/server/http/tests/test_httpdate.py src/zope/server/http/tests/test_httprequestparser.py src/zope/server/http/tests/test_httpserver.py src/zope/server/http/tests/test_wsgiserver.py src/zope/server/http/tests/wsgi_app.py src/zope/server/interfaces/__init__.py src/zope/server/interfaces/ftp.py src/zope/server/interfaces/logger.py src/zope/server/linereceiver/__init__.py src/zope/server/linereceiver/linecommandparser.py src/zope/server/linereceiver/lineserverchannel.py src/zope/server/linereceiver/linetask.py src/zope/server/logger/__init__.py src/zope/server/logger/filelogger.py src/zope/server/logger/m_syslog.py src/zope/server/logger/pythonlogger.py src/zope/server/logger/resolvinglogger.py src/zope/server/logger/rotatingfilelogger.py src/zope/server/logger/socketlogger.py src/zope/server/logger/sysloglogger.py src/zope/server/logger/taillogger.py src/zope/server/logger/unresolvinglogger.py src/zope/server/logger/tests/__init__.py src/zope/server/logger/tests/test_pythonlogger.py src/zope/server/tests/__init__.py src/zope/server/tests/asyncerror.py src/zope/server/tests/test_serverbase.py src/zope/server/tests/test_zombies.pyzope.server-3.8.6/src/zope.server.egg-info/dependency_links.txt0000664000175000017500000000000111701734346023663 0ustar mgmg00000000000000 zope.server-3.8.6/src/zope.server.egg-info/top_level.txt0000664000175000017500000000000511701734346022342 0ustar mgmg00000000000000zope zope.server-3.8.6/src/zope/0000775000175000017500000000000011701734350014611 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/0000775000175000017500000000000011701734350016117 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/buffers.py0000664000175000017500000001445511701734341020136 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001-2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Buffers """ try: from cStringIO import StringIO except ImportError: from StringIO import StringIO # copy_bytes controls the size of temp. strings for shuffling data around. COPY_BYTES = 1 << 18 # 256K # The maximum number of bytes to buffer in a simple string. STRBUF_LIMIT = 8192 class FileBasedBuffer(object): remain = 0 def __init__(self, file, from_buffer=None): self.file = file if from_buffer is not None: from_file = from_buffer.getfile() read_pos = from_file.tell() from_file.seek(0) while 1: data = from_file.read(COPY_BYTES) if not data: break file.write(data) self.remain = int(file.tell() - read_pos) from_file.seek(read_pos) file.seek(read_pos) def __len__(self): return self.remain def append(self, s): file = self.file read_pos = file.tell() file.seek(0, 2) file.write(s) file.seek(read_pos) self.remain = self.remain + len(s) def get(self, bytes=-1, skip=0): file = self.file if not skip: read_pos = file.tell() if bytes < 0: # Read all res = file.read() else: res = file.read(bytes) if skip: self.remain -= len(res) else: file.seek(read_pos) return res def skip(self, bytes, allow_prune=0): if self.remain < bytes: raise ValueError("Can't skip %d bytes in buffer of %d bytes" % ( bytes, self.remain)) self.file.seek(bytes, 1) self.remain = self.remain - bytes def newfile(self): raise NotImplementedError() def prune(self): file = self.file if self.remain == 0: read_pos = file.tell() file.seek(0, 2) sz = file.tell() file.seek(read_pos) if sz == 0: # Nothing to prune. return nf = self.newfile() while 1: data = file.read(COPY_BYTES) if not data: break nf.write(data) self.file = nf def getfile(self): return self.file class TempfileBasedBuffer(FileBasedBuffer): def __init__(self, from_buffer=None): FileBasedBuffer.__init__(self, self.newfile(), from_buffer) def newfile(self): from tempfile import TemporaryFile return TemporaryFile('w+b') class StringIOBasedBuffer(FileBasedBuffer): def __init__(self, from_buffer=None): if from_buffer is not None: FileBasedBuffer.__init__(self, StringIO(), from_buffer) else: # Shortcut. :-) self.file = StringIO() def newfile(self): return StringIO() class OverflowableBuffer(object): """ This buffer implementation has four stages: - No data - String-based buffer - StringIO-based buffer - Temporary file storage The first two stages are fastest for simple transfers. """ overflowed = 0 buf = None strbuf = '' # String-based buffer. def __init__(self, overflow): # overflow is the maximum to be stored in a StringIO buffer. self.overflow = overflow def __len__(self): buf = self.buf if buf is not None: return len(buf) else: return len(self.strbuf) def _create_buffer(self): # print 'creating buffer' strbuf = self.strbuf if len(strbuf) >= self.overflow: self._set_large_buffer() else: self._set_small_buffer() buf = self.buf if strbuf: buf.append(self.strbuf) self.strbuf = '' return buf def _set_small_buffer(self): self.buf = StringIOBasedBuffer(self.buf) self.overflowed = 0 def _set_large_buffer(self): self.buf = TempfileBasedBuffer(self.buf) self.overflowed = 1 def append(self, s): buf = self.buf if buf is None: strbuf = self.strbuf if len(strbuf) + len(s) < STRBUF_LIMIT: self.strbuf = strbuf + s return buf = self._create_buffer() buf.append(s) sz = len(buf) if not self.overflowed: if sz >= self.overflow: self._set_large_buffer() def get(self, bytes=-1, skip=0): buf = self.buf if buf is None: strbuf = self.strbuf if not skip: return strbuf buf = self._create_buffer() return buf.get(bytes, skip) def skip(self, bytes, allow_prune=0): buf = self.buf if buf is None: strbuf = self.strbuf if allow_prune and bytes == len(strbuf): # We could slice instead of converting to # a buffer, but that would eat up memory in # large transfers. self.strbuf = '' return buf = self._create_buffer() buf.skip(bytes, allow_prune) def prune(self): """ A potentially expensive operation that removes all data already retrieved from the buffer. """ buf = self.buf if buf is None: self.strbuf = '' return buf.prune() if self.overflowed: sz = len(buf) if sz < self.overflow: # Revert to a faster buffer. self._set_small_buffer() def getfile(self): buf = self.buf if buf is None: buf = self._create_buffer() return buf.getfile() zope.server-3.8.6/src/zope/server/maxsockets.py0000664000175000017500000000475311701734341020663 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Find max number of sockets allowed. """ # Medusa max_sockets module. import socket import select # several factors here we might want to test: # 1) max we can create # 2) max we can bind # 3) max we can listen on # 4) max we can connect def max_server_sockets(): # TODO: This should be a configuration value as it takes a long time to # compute on Mac OSX return 100 sl = [] while 1: try: s = socket.socket (socket.AF_INET, socket.SOCK_STREAM) s.bind (('',0)) s.listen(5) sl.append (s) except: break num = len(sl) for s in sl: s.close() del sl return num def max_client_sockets(): # TODO: This should be a configuration value as it takes a long time to # compute on Mac OSX return 100 # make a server socket server = socket.socket (socket.AF_INET, socket.SOCK_STREAM) server.bind (('', 9999)) server.listen (5) sl = [] while 1: try: s = socket.socket (socket.AF_INET, socket.SOCK_STREAM) s.connect (('', 9999)) conn, addr = server.accept() sl.append ((s,conn)) except: break num = len(sl) for s,c in sl: s.close() c.close() del sl return num def max_select_sockets(): # TODO: This should be a configuration value as it takes a long time to # compute on Mac OSX return 100 sl = [] while 1: try: num = len(sl) for i in range(1 + len(sl) // 20): # Increase exponentially. s = socket.socket (socket.AF_INET, socket.SOCK_STREAM) s.bind (('',0)) s.listen(5) sl.append (s) select.select(sl,[],[],0) except: break for s in sl: s.close() del sl return num zope.server-3.8.6/src/zope/server/logger/0000775000175000017500000000000011701734350017376 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/logger/rotatingfilelogger.py0000664000175000017500000000710311701734337023645 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Rotating File Logger Rotates a log file in time intervals. """ import time import os import stat from zope.server.logger.filelogger import FileLogger class RotatingFileLogger(FileLogger): """ If freq is non-None we back up 'daily', 'weekly', or 'monthly'. Else if maxsize is non-None we back up whenever the log gets to big. If both are None we never back up. Like a FileLogger, but it must be attached to a filename. When the log gets too full, or a certain time has passed, it backs up the log and starts a new one. Note that backing up the log is done via 'mv' because anything else (cp, gzip) would take time, during which medusa would do nothing else. """ def __init__(self, file, freq=None, maxsize=None, flush=1, mode='a'): self.filename = file self.mode = mode self.file = open(file, mode) self.freq = freq self.maxsize = maxsize self.rotate_when = self.next_backup(self.freq) self.do_flush = flush def __repr__(self): return '' % self.file # We back up at midnight every 1) day, 2) monday, or 3) 1st of month def next_backup(self, freq): (yr, mo, day, hr, min, sec, wd, jday, dst) = \ time.localtime(time.time()) if freq == 'daily': return time.mktime((yr,mo,day+1, 0,0,0, 0,0,-1)) elif freq == 'weekly': # wd(monday)==0 return time.mktime((yr,mo,day-wd+7, 0,0,0, 0,0,-1)) elif freq == 'monthly': return time.mktime((yr,mo+1,1, 0,0,0, 0,0,-1)) else: return None # not a date-based backup def maybe_flush(self): # rotate first if necessary self.maybe_rotate() if self.do_flush: # from file_logger() self.file.flush() def maybe_rotate(self): if self.freq and time.time() > self.rotate_when: self.rotate() self.rotate_when = self.next_backup(self.freq) elif self.maxsize: # rotate when we get too big try: if os.stat(self.filename)[stat.ST_SIZE] > self.maxsize: self.rotate() except os.error: # file not found, probably self.rotate() # will create a new file def rotate(self): yr, mo, day, hr, min, sec, wd, jday, dst = time.localtime(time.time()) try: self.file.close() newname = '%s.ends%04d%02d%02d' % (self.filename, yr, mo, day) try: open(newname, "r").close() # check if file exists newname = newname + "-%02d%02d%02d" % (hr, min, sec) except IOError: # concatenation of YEAR MO DY is unique pass os.rename(self.filename, newname) self.file = open(self.filename, self.mode) except IOError: pass zope.server-3.8.6/src/zope/server/logger/m_syslog.py0000664000175000017500000001567511701734337021627 0ustar mgmg00000000000000# -*- Mode: Python; tab-width: 4 -*- # ====================================================================== # Copyright 1997 by Sam Rushing # # All Rights Reserved # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee is hereby # granted, provided that the above copyright notice appear in all # copies and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of Sam # Rushing not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. # # SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN # NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # ====================================================================== """socket interface to unix syslog. On Unix, there are usually two ways of getting to syslog: via a local unix-domain socket, or via the TCP service. Usually "/dev/log" is the unix domain socket. This may be different for other systems. >>> my_client = syslog_client ('/dev/log') Otherwise, just use the UDP version, port 514. >>> my_client = syslog_client (('my_log_host', 514)) On win32, you will have to use the UDP version. Note that you can use this to log to other hosts (and indeed, multiple hosts). This module is not a drop-in replacement for the python extension module - the interface is different. Usage: >>> c = syslog_client() >>> c = syslog_client ('/strange/non_standard_log_location') >>> c = syslog_client (('other_host.com', 514)) >>> c.log ('testing', facility='local0', priority='debug') """ # TODO: support named-pipe syslog. # [see ftp://sunsite.unc.edu/pub/Linux/system/Daemons/syslog-fifo.tar.z] # from : # =========================================================================== # priorities/facilities are encoded into a single 32-bit quantity, where the # bottom 3 bits are the priority (0-7) and the top 28 bits are the facility # (0-big number). Both the priorities and the facilities map roughly # one-to-one to strings in the syslogd(8) source code. This mapping is # included in this file. # # priorities (these are ordered) LOG_EMERG = 0 # system is unusable LOG_ALERT = 1 # action must be taken immediately LOG_CRIT = 2 # critical conditions LOG_ERR = 3 # error conditions LOG_WARNING = 4 # warning conditions LOG_NOTICE = 5 # normal but significant condition LOG_INFO = 6 # informational LOG_DEBUG = 7 # debug-level messages # facility codes LOG_KERN = 0 # kernel messages LOG_USER = 1 # random user-level messages LOG_MAIL = 2 # mail system LOG_DAEMON = 3 # system daemons LOG_AUTH = 4 # security/authorization messages LOG_SYSLOG = 5 # messages generated internally by syslogd LOG_LPR = 6 # line printer subsystem LOG_NEWS = 7 # network news subsystem LOG_UUCP = 8 # UUCP subsystem LOG_CRON = 9 # clock daemon LOG_AUTHPRIV = 10 # security/authorization messages (private) # other codes through 15 reserved for system use LOG_LOCAL0 = 16 # reserved for local use LOG_LOCAL1 = 17 # reserved for local use LOG_LOCAL2 = 18 # reserved for local use LOG_LOCAL3 = 19 # reserved for local use LOG_LOCAL4 = 20 # reserved for local use LOG_LOCAL5 = 21 # reserved for local use LOG_LOCAL6 = 22 # reserved for local use LOG_LOCAL7 = 23 # reserved for local use priority_names = { "alert": LOG_ALERT, "crit": LOG_CRIT, "debug": LOG_DEBUG, "emerg": LOG_EMERG, "err": LOG_ERR, "error": LOG_ERR, # DEPRECATED "info": LOG_INFO, "notice": LOG_NOTICE, "panic": LOG_EMERG, # DEPRECATED "warn": LOG_WARNING, # DEPRECATED "warning": LOG_WARNING, } facility_names = { "auth": LOG_AUTH, "authpriv": LOG_AUTHPRIV, "cron": LOG_CRON, "daemon": LOG_DAEMON, "kern": LOG_KERN, "lpr": LOG_LPR, "mail": LOG_MAIL, "news": LOG_NEWS, "security": LOG_AUTH, # DEPRECATED "syslog": LOG_SYSLOG, "user": LOG_USER, "uucp": LOG_UUCP, "local0": LOG_LOCAL0, "local1": LOG_LOCAL1, "local2": LOG_LOCAL2, "local3": LOG_LOCAL3, "local4": LOG_LOCAL4, "local5": LOG_LOCAL5, "local6": LOG_LOCAL6, "local7": LOG_LOCAL7, } import socket class syslog_client(object): def __init__(self, address='/dev/log'): self.address = address if type(address) == type(''): try: # APUE 13.4.2 specifes /dev/log as datagram socket self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self.socket.connect(address) except: # older linux may create as stream socket self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket.connect(address) self.unix = 1 else: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.unix = 0 log_format_string = '<%d>%s\000' def log(self, message, facility=LOG_USER, priority=LOG_INFO): message = self.log_format_string % ( self.encode_priority(facility, priority), message ) if self.unix: self.socket.send(message) else: self.socket.sendto(message, self.address) def encode_priority(self, facility, priority): if type(facility) == type(''): facility = facility_names[facility] if type(priority) == type(''): priority = priority_names[priority] return (facility<<3) | priority def close(self): if self.unix: self.socket.close() zope.server-3.8.6/src/zope/server/logger/resolvinglogger.py0000664000175000017500000000333211701734337023166 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Resolving Logger """ from zope.server.interfaces.logger import IRequestLogger from zope.interface import implements class ResolvingLogger(object): """Feed (ip, message) combinations into this logger to get a resolved hostname in front of the message. The message will not be logged until the PTR request finishes (or fails).""" implements(IRequestLogger) def __init__(self, resolver, logger): self.resolver = resolver # logger is an IMessageLogger self.logger = logger class logger_thunk(object): def __init__(self, message, logger): self.message = message self.logger = logger def __call__(self, host, ttl, answer): if not answer: answer = host self.logger.logMessage('%s%s' % (answer, self.message)) def logRequest(self, ip, message): 'See IRequestLogger' self.resolver.resolve_ptr( ip, self.logger_thunk( message, self.logger ) ) zope.server-3.8.6/src/zope/server/logger/socketlogger.py0000664000175000017500000000275611701734337022457 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Socket Logger Sends logging messages to a socket. """ import asynchat import socket from zope.server.interfaces.logger import IMessageLogger from zope.interface import implements class SocketLogger(asynchat.async_chat): """Log to a stream socket, asynchronously.""" implements(IMessageLogger) def __init__(self, address): if type(address) == type(''): self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) else: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect(address) self.address = address def __repr__(self): return '' % (self.address) def logMessage(self, message): 'See IMessageLogger' if message[-2:] != '\r\n': self.socket.push(message + '\r\n') else: self.socket.push(message) zope.server-3.8.6/src/zope/server/logger/sysloglogger.py0000664000175000017500000000354211701734337022501 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Syslog Logger Writes log messages to syslog. """ import os from zope.server.logger import m_syslog from zope.server.interfaces.logger import IMessageLogger from zope.interface import implements class SyslogLogger(m_syslog.syslog_client): """syslog is a line-oriented log protocol - this class would be appropriate for FTP or HTTP logs, but not for dumping stderr to. TODO: a simple safety wrapper that will ensure that the line sent to syslog is reasonable. TODO: async version of syslog_client: now, log entries use blocking send() """ implements(IMessageLogger) svc_name = 'zope' pid_str = str(os.getpid()) def __init__ (self, address, facility='user'): m_syslog.syslog_client.__init__ (self, address) self.facility = m_syslog.facility_names[facility] self.address=address def __repr__ (self): return '' % (repr(self.address)) def logMessage(self, message): 'See IMessageLogger' m_syslog.syslog_client.log ( self, '%s[%s]: %s' % (self.svc_name, self.pid_str, message), facility=self.facility, priority=m_syslog.LOG_INFO ) zope.server-3.8.6/src/zope/server/logger/tests/0000775000175000017500000000000011701734350020540 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/logger/tests/test_pythonlogger.py0000664000175000017500000000411011701734336024672 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Python Logger tests """ import unittest import logging from zope.interface.verify import verifyObject class HandlerStub(logging.Handler): last_record = None def emit(self, record): self.last_record = record class TestPythonLogger(unittest.TestCase): name = 'test.pythonlogger' def setUp(self): self.logger = logging.getLogger(self.name) self.logger.propagate = False self.logger.setLevel(logging.INFO) self.handler = HandlerStub() self.logger.addHandler(self.handler) def tearDown(self): self.logger.removeHandler(self.handler) def test(self): from zope.server.logger.pythonlogger import PythonLogger from zope.server.interfaces.logger import IMessageLogger plogger = PythonLogger(self.name) verifyObject(IMessageLogger, plogger) msg1 = 'test message 1' plogger.logMessage(msg1) self.assertEquals(self.handler.last_record.msg, msg1) self.assertEquals(self.handler.last_record.levelno, logging.INFO) msg2 = 'test message 2\r\n' plogger.level = logging.ERROR plogger.logMessage(msg2) self.assertEquals(self.handler.last_record.msg, msg2.rstrip()) self.assertEquals(self.handler.last_record.levelno, logging.ERROR) def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestPythonLogger)) return suite if __name__ == '__main__': unittest.main() zope.server-3.8.6/src/zope/server/logger/tests/__init__.py0000664000175000017500000000007511701734336022657 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/logger/pythonlogger.py0000664000175000017500000000246711701734337022507 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Proxy between the server's and Python's logger interfaces. """ import logging from zope.server.interfaces.logger import IMessageLogger from zope.interface import implements class PythonLogger(object): """Proxy for Python's logging module""" implements(IMessageLogger) def __init__(self, name=None, level=logging.INFO): self.name = name self.level = level self.logger = logging.getLogger(name) def __repr__(self): return '' % (self.name, logging.getLevelName(self.level)) def logMessage(self, message): """See IMessageLogger""" self.logger.log(self.level, message.rstrip()) zope.server-3.8.6/src/zope/server/logger/taillogger.py0000664000175000017500000000245511701734337022114 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Tail Logger """ from zope.server.interfaces.logger import IMessageLogger from zope.interface import implements class TailLogger(object): """Keep track of the last log messages""" implements(IMessageLogger) def __init__(self, logger, size=500): self.size = size self.logger = logger self.messages = [] def logMessage(self, message): 'See IMessageLogger' self.messages.append(strip_eol(message)) if len(self.messages) > self.size: del self.messages[0] self.logger.logMessage(message) def strip_eol(line): while line and line[-1] in '\r\n': line = line[:-1] return line zope.server-3.8.6/src/zope/server/logger/unresolvinglogger.py0000664000175000017500000000205011701734337023525 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Unresolving Logger """ from zope.server.interfaces.logger import IRequestLogger from zope.interface import implements class UnresolvingLogger(object): """Just in case you don't want to resolve""" implements(IRequestLogger) def __init__(self, logger): self.logger = logger def logRequest(self, ip, message): 'See IRequestLogger' self.logger.logMessage('%s%s' % (ip, message)) zope.server-3.8.6/src/zope/server/logger/__init__.py0000664000175000017500000000007511701734337021516 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/logger/filelogger.py0000664000175000017500000000366511701734337022106 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """File Logger """ from types import StringType from zope.server.interfaces.logger import IMessageLogger from zope.interface import implements class FileLogger(object): """Simple File Logger """ implements(IMessageLogger) def __init__(self, file, flush=1, mode='a'): """pass this either a path or a file object.""" if type(file) is StringType: if (file == '-'): import sys self.file = sys.stdout else: self.file = open(file, mode) else: self.file = file self.do_flush = flush def __repr__(self): return '' % self.file def write(self, data): self.file.write(data) self.maybe_flush() def writeline(self, line): self.file.writeline(line) self.maybe_flush() def writelines(self, lines): self.file.writelines(lines) self.maybe_flush() def maybe_flush(self): if self.do_flush: self.file.flush() def flush(self): self.file.flush() def softspace(self, *args): pass def logMessage(self, message): 'See IMessageLogger' if message[-1] not in ('\r', '\n'): self.write(message + '\n') else: self.write(message) zope.server-3.8.6/src/zope/server/taskthreads.py0000664000175000017500000001004411701734341021005 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Threaded Task Dispatcher """ from Queue import Queue, Empty from thread import allocate_lock, start_new_thread from time import time, sleep import logging from zope.server.interfaces import ITaskDispatcher from zope.interface import implements log = logging.getLogger(__name__) class ThreadedTaskDispatcher(object): """A Task Dispatcher that creates a thread for each task.""" implements(ITaskDispatcher) stop_count = 0 # Number of threads that will stop soon. def __init__(self): self.threads = {} # { thread number -> 1 } self.queue = Queue() self.thread_mgmt_lock = allocate_lock() def handlerThread(self, thread_no): threads = self.threads try: while threads.get(thread_no): task = self.queue.get() if task is None: # Special value: kill this thread. break try: task.service() except: log.exception('Exception during task') finally: mlock = self.thread_mgmt_lock mlock.acquire() try: self.stop_count -= 1 try: del threads[thread_no] except KeyError: pass finally: mlock.release() def setThreadCount(self, count): """See zope.server.interfaces.ITaskDispatcher""" mlock = self.thread_mgmt_lock mlock.acquire() try: threads = self.threads thread_no = 0 running = len(threads) - self.stop_count while running < count: # Start threads. while thread_no in threads: thread_no = thread_no + 1 threads[thread_no] = 1 running += 1 start_new_thread(self.handlerThread, (thread_no,)) thread_no = thread_no + 1 if running > count: # Stop threads. to_stop = running - count self.stop_count += to_stop for n in range(to_stop): self.queue.put(None) running -= 1 finally: mlock.release() def addTask(self, task): """See zope.server.interfaces.ITaskDispatcher""" if task is None: raise ValueError("No task passed to addTask().") # assert ITask.providedBy(task) try: task.defer() self.queue.put(task) except: task.cancel() raise def shutdown(self, cancel_pending=True, timeout=5): """See zope.server.interfaces.ITaskDispatcher""" self.setThreadCount(0) # Ensure the threads shut down. threads = self.threads expiration = time() + timeout while threads: if time() >= expiration: log.error("%d thread(s) still running" % len(threads)) break sleep(0.1) if cancel_pending: # Cancel remaining tasks. try: queue = self.queue while not queue.empty(): task = queue.get() if task is not None: task.cancel() except Empty: pass def getPendingTasksEstimate(self): """See zope.server.interfaces.ITaskDispatcher""" return self.queue.qsize() zope.server-3.8.6/src/zope/server/serverchannelbase.py0000664000175000017500000001551411701734341022171 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Server-Channel Base Class This module provides a base implementation for the server channel. It can only be used as a mix-in to actual server channel implementations. """ import os import time import sys import asyncore from thread import allocate_lock from zope.interface import implements from zope.server.dualmodechannel import DualModeChannel from zope.server.interfaces import IServerChannel, ITask # task_lock is useful for synchronizing access to task-related attributes. task_lock = allocate_lock() class ServerChannelBase(DualModeChannel, object): """Base class for a high-performance, mixed-mode server-side channel.""" implements(IServerChannel, ITask) # See zope.server.interfaces.IServerChannel parser_class = None # Subclasses must provide a parser class task_class = None # ... and a task class. active_channels = {} # Class-specific channel tracker next_channel_cleanup = [0] # Class-specific cleanup time proto_request = None # A request parser instance last_activity = 0 # Time of last activity tasks = None # List of channel-related tasks to execute running_tasks = False # True when another thread is running tasks # # ASYNCHRONOUS METHODS (including __init__) # def __init__(self, server, conn, addr, adj=None): """See async.dispatcher""" DualModeChannel.__init__(self, conn, addr, adj) self.server = server self.last_activity = t = self.creation_time self.check_maintenance(t) def add_channel(self, map=None): """See async.dispatcher This hook keeps track of opened channels. """ DualModeChannel.add_channel(self, map) self.__class__.active_channels[self._fileno] = self def del_channel(self, map=None): """See async.dispatcher This hook keeps track of closed channels. """ DualModeChannel.del_channel(self, map) ac = self.__class__.active_channels fd = self._fileno if fd in ac: del ac[fd] def check_maintenance(self, now): """See async.dispatcher Performs maintenance if necessary. """ ncc = self.__class__.next_channel_cleanup if now < ncc[0]: return ncc[0] = now + self.adj.cleanup_interval self.maintenance() def maintenance(self): """See async.dispatcher Kills off dead connections. """ self.kill_zombies() def kill_zombies(self): """See async.dispatcher Closes connections that have not had any activity in a while. The timeout is configured through adj.channel_timeout (seconds). """ now = time.time() cutoff = now - self.adj.channel_timeout for channel in self.active_channels.values(): if (channel is not self and not channel.running_tasks and channel.last_activity < cutoff): channel.close() def received(self, data): """See async.dispatcher Receives input asynchronously and send requests to handle_request(). """ preq = self.proto_request while data: if preq is None: preq = self.parser_class(self.adj) n = preq.received(data) if preq.completed: # The request is ready to use. self.proto_request = None if not preq.empty: self.handle_request(preq) preq = None else: self.proto_request = preq if n >= len(data): break data = data[n:] def handle_request(self, req): """Creates and queues a task for processing a request. Subclasses may override this method to handle some requests immediately in the main async thread. """ task = self.task_class(self, req) self.queue_task(task) def handle_error(self): """See async.dispatcher Handles program errors (not communication errors) """ t, v = sys.exc_info()[:2] if t is SystemExit or t is KeyboardInterrupt: raise t(v) asyncore.dispatcher.handle_error(self) def handle_comm_error(self): """See async.dispatcher Handles communication errors (not program errors) """ if self.adj.log_socket_errors: self.handle_error() else: # Ignore socket errors. self.close() # # BOTH MODES # def queue_task(self, task): """Queue a channel-related task to be executed in another thread.""" start = False task_lock.acquire() try: if self.tasks is None: self.tasks = [] self.tasks.append(task) if not self.running_tasks: self.running_tasks = True start = True finally: task_lock.release() if start: self.set_sync() self.server.addTask(self) # # ITask implementation. Delegates to the queued tasks. # def service(self): """Execute all pending tasks""" while True: task = None task_lock.acquire() try: if self.tasks: task = self.tasks.pop(0) else: # No more tasks self.running_tasks = False self.set_async() break finally: task_lock.release() try: task.service() except: # propagate the exception, but keep executing tasks self.server.addTask(self) raise def cancel(self): """Cancels all pending tasks""" task_lock.acquire() try: if self.tasks: old = self.tasks[:] else: old = [] self.tasks = [] self.running_tasks = False finally: task_lock.release() try: for task in old: task.cancel() finally: self.set_async() def defer(self): pass zope.server-3.8.6/src/zope/server/zlogintegration.py0000664000175000017500000000212511701734341021710 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Make asyncore log to the logging module. As a side effect of importing this module, asyncore's logging will be redirected to the logging module. """ import logging logger = logging.getLogger("zope.server") severity = { 'info': logging.INFO, 'warning': logging.WARN, 'error': logging.ERROR, } def log_info(self, message, type='info'): logger.log(severity.get(type, logging.INFO), message) import asyncore asyncore.dispatcher.log_info = log_info zope.server-3.8.6/src/zope/server/linereceiver/0000775000175000017500000000000011701734350020573 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/linereceiver/linetask.py0000664000175000017500000000373211701734341022764 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Line Task """ import socket import time from zope.server.interfaces import ITask from zope.interface import implements class LineTask(object): """This is a generic task that can be used with command line protocols to handle commands in a separate thread. """ implements(ITask) def __init__(self, channel, command, m_name): self.channel = channel self.m_name = m_name self.args = command.args self.close_on_finish = 0 def service(self): """Called to execute the task. """ try: try: self.start() getattr(self.channel, self.m_name)(self.args) self.finish() except socket.error: self.close_on_finish = 1 if self.channel.adj.log_socket_errors: raise except: self.channel.exception() finally: if self.close_on_finish: self.channel.close_when_done() def cancel(self): 'See ITask' self.channel.close_when_done() def defer(self): 'See ITask' pass def start(self): now = time.time() self.start_time = now def finish(self): hit_log = self.channel.server.hit_log if hit_log is not None: hit_log.log(self) zope.server-3.8.6/src/zope/server/linereceiver/linecommandparser.py0000664000175000017500000000371311701734341024654 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Line Command Parser """ from zope.server.interfaces import IStreamConsumer from zope.interface import implements class LineCommandParser(object): """Line Command parser. Arguments are left alone for now.""" implements(IStreamConsumer) # See IStreamConsumer completed = 0 inbuf = '' cmd = '' args = '' empty = 0 max_line_length = 1024 # Not a hard limit def __init__(self, adj): """ adj is an Adjustments object. """ self.adj = adj def received(self, data): 'See IStreamConsumer' if self.completed: return 0 # Can't consume any more. pos = data.find('\n') datalen = len(data) if pos < 0: self.inbuf = self.inbuf + data if len(self.inbuf) > self.max_line_length: # Don't accept any more. self.completed = 1 return datalen else: # Line finished. s = data[:pos + 1] self.inbuf = self.inbuf + s self.completed = 1 line = self.inbuf.strip() self.parseLine(line) return len(s) def parseLine(self, line): parts = line.split(' ', 1) if len(parts) == 2: self.cmd, self.args = parts else: self.cmd = parts[0] zope.server-3.8.6/src/zope/server/linereceiver/lineserverchannel.py0000664000175000017500000001060411701734341024655 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Line receiver channel This channels evaluates requests line by line. This is particular useful for protocols that use a line-based command structure. """ from asyncore import compact_traceback import os import sys from zope.server.serverchannelbase import ServerChannelBase from zope.server.linereceiver.linecommandparser import LineCommandParser from zope.server.linereceiver.linetask import LineTask DEBUG = os.environ.get('ZOPE_SERVER_DEBUG') class LineServerChannel(ServerChannelBase): """The Line Server Channel represents a connection to a particular client. We can therefore store information here.""" # Wrapper class that is used to execute a command in a different thread task_class = LineTask # Class that is being initialized to parse the input parser_class = LineCommandParser # List of commands that are always available special_commands = ('cmd_quit') # Commands that are run in a separate thread thread_commands = () # Define the authentication status of the channel. Note that only the # "special commands" can be executed without having authenticated. authenticated = 0 # Define the reply code for non-authenticated responses not_auth_reply = 'LOGIN_REQUIRED' # Define the reply code for an unrecognized command unknown_reply = 'CMD_UNKNOWN' # Define the error message that occurs, when the reply code was not found. reply_error = '500 Unknown Reply Code: %s.' # Define the status messages status_messages = { 'CMD_UNKNOWN' : "500 '%s': command not understood.", 'INTERNAL_ERROR' : "500 Internal error: %s", 'LOGIN_REQUIRED' : '530 Please log in with USER and PASS', } def handle_request(self, command): """Processes a command. Some commands use an alternate thread. """ assert isinstance(command, LineCommandParser) cmd = command.cmd method = 'cmd_' + cmd.lower() if (not self.authenticated and method not in self.special_commands): # The user is not logged in, therefore don't allow anything self.reply(self.not_auth_reply) elif method in self.thread_commands: # Process in another thread. task = self.task_class(self, command, method) self.queue_task(task) elif hasattr(self, method): try: getattr(self, method)(command.args) except: self.exception() else: self.reply(self.unknown_reply, cmd.upper()) def reply(self, code, args=(), flush=1): """ """ try: msg = self.status_messages[code] %args except: msg = self.reply_error %code self.write('%s\r\n' %msg) if flush: self.flush(0) # TODO: Some logging should go on here. def handle_error_no_close(self): """See asyncore.dispatcher.handle_error()""" nil, t, v, tbinfo = compact_traceback() # sometimes a user repr method will crash. try: self_repr = repr(self) except: self_repr = '<__repr__(self) failed for object at %0x>' % id(self) self.log_info( 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( self_repr, t, v, tbinfo ), 'error' ) def exception(self): if DEBUG: import traceback traceback.print_exc() t, v = sys.exc_info()[:2] try: info = '%s: %s' % (getattr(t, '__name__', t), v) except: info = str(t) self.reply('INTERNAL_ERROR', info) self.handle_error_no_close() self.close_when_done() zope.server-3.8.6/src/zope/server/linereceiver/__init__.py0000664000175000017500000000007511701734341022706 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/trigger.py0000664000175000017500000002063211701734341020137 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001-2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## import asyncore import os import socket import struct import thread import errno _ADDRESS_MASK = 256 ** struct.calcsize('P') def positive_id(obj): """Return id(obj) as a non-negative integer.""" result = id(obj) if result < 0: result += _ADDRESS_MASK assert result > 0 return result # Original comments follow; they're hard to follow in the context of # ZEO's use of triggers. TODO: rewrite from a ZEO perspective. # Wake up a call to select() running in the main thread. # # This is useful in a context where you are using Medusa's I/O # subsystem to deliver data, but the data is generated by another # thread. Normally, if Medusa is in the middle of a call to # select(), new output data generated by another thread will have # to sit until the call to select() either times out or returns. # If the trigger is 'pulled' by another thread, it should immediately # generate a READ event on the trigger object, which will force the # select() invocation to return. # # A common use for this facility: letting Medusa manage I/O for a # large number of connections; but routing each request through a # thread chosen from a fixed-size thread pool. When a thread is # acquired, a transaction is performed, but output data is # accumulated into buffers that will be emptied more efficiently # by Medusa. [picture a server that can process database queries # rapidly, but doesn't want to tie up threads waiting to send data # to low-bandwidth connections] # # The other major feature provided by this class is the ability to # move work back into the main thread: if you call pull_trigger() # with a thunk argument, when select() wakes up and receives the # event it will call your thunk from within that thread. The main # purpose of this is to remove the need to wrap thread locks around # Medusa's data structures, which normally do not need them. [To see # why this is true, imagine this scenario: A thread tries to push some # new data onto a channel's outgoing data queue at the same time that # the main thread is trying to remove some] class _triggerbase(object): """OS-independent base class for OS-dependent trigger class.""" kind = None # subclass must set to "pipe" or "loopback"; used by repr def __init__(self): self._closed = False # `lock` protects the `thunks` list from being traversed and # appended to simultaneously. self.lock = thread.allocate_lock() # List of no-argument callbacks to invoke when the trigger is # pulled. These run in the thread running the asyncore mainloop, # regardless of which thread pulls the trigger. self.thunks = [] def readable(self): return 1 def writable(self): return 0 def handle_connect(self): pass def handle_close(self): self.close() # Override the asyncore close() method, because it doesn't know about # (so can't close) all the gimmicks we have open. Subclass must # supply a _close() method to do platform-specific closing work. _close() # will be called iff we're not already closed. def close(self): if not self._closed: self._closed = True self.del_channel() self._close() # subclass does OS-specific stuff def _close(self): # see close() above; subclass must supply raise NotImplementedError def pull_trigger(self, thunk=None): if thunk: self.lock.acquire() try: self.thunks.append(thunk) finally: self.lock.release() self._physical_pull() # Subclass must supply _physical_pull, which does whatever the OS # needs to do to provoke the "write" end of the trigger. def _physical_pull(self): raise NotImplementedError def handle_read(self): try: self.recv(8192) except socket.error: return self.lock.acquire() try: for thunk in self.thunks: try: thunk() except: nil, t, v, tbinfo = asyncore.compact_traceback() print ('exception in trigger thunk:' ' (%s:%s %s)' % (t, v, tbinfo)) self.thunks = [] finally: self.lock.release() def __repr__(self): return '' % (self.kind, positive_id(self)) if os.name == 'posix': class trigger(_triggerbase, asyncore.file_dispatcher): kind = "pipe" def __init__(self): _triggerbase.__init__(self) r, self.trigger = self._fds = os.pipe() asyncore.file_dispatcher.__init__(self, r) def _close(self): for fd in self._fds: os.close(fd) self._fds = [] def _physical_pull(self): os.write(self.trigger, 'x') else: # Windows version; uses just sockets, because a pipe isn't select'able # on Windows. class trigger(_triggerbase, asyncore.dispatcher): kind = "loopback" def __init__(self): _triggerbase.__init__(self) # Get a pair of connected sockets. The trigger is the 'w' # end of the pair, which is connected to 'r'. 'r' is put # in the asyncore socket map. "pulling the trigger" then # means writing something on w, which will wake up r. w = socket.socket() # Disable buffering -- pulling the trigger sends 1 byte, # and we want that sent immediately, to wake up asyncore's # select() ASAP. w.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) count = 0 while 1: count += 1 # Bind to a local port; for efficiency, let the OS pick # a free port for us. # Unfortunately, stress tests showed that we may not # be able to connect to that port ("Address already in # use") despite that the OS picked it. This appears # to be a race bug in the Windows socket implementation. # So we loop until a connect() succeeds (almost always # on the first try). See the long thread at # http://mail.zope.org/pipermail/zope/2005-July/160433.html # for hideous details. a = socket.socket() a.bind(("127.0.0.1", 0)) connect_address = a.getsockname() # assigned (host, port) pair a.listen(1) try: w.connect(connect_address) break # success except socket.error, detail: if detail[0] != errno.WSAEADDRINUSE: # "Address already in use" is the only error # I've seen on two WinXP Pro SP2 boxes, under # Pythons 2.3.5 and 2.4.1. raise # (10048, 'Address already in use') # assert count <= 2 # never triggered in Tim's tests if count >= 10: # I've never seen it go above 2 a.close() w.close() raise BindError("Cannot bind trigger!") # Close `a` and try again. Note: I originally put a short # sleep() here, but it didn't appear to help or hurt. a.close() r, addr = a.accept() # r becomes asyncore's (self.)socket a.close() self.trigger = w asyncore.dispatcher.__init__(self, r) def _close(self): # self.socket is r, and self.trigger is w, from __init__ self.socket.close() self.trigger.close() def _physical_pull(self): self.trigger.send('x') zope.server-3.8.6/src/zope/server/ftp/0000775000175000017500000000000011701734350016710 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/ftp/server.py0000664000175000017500000007456311701734341020607 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """FTP Server """ import asyncore import posixpath import socket from datetime import date, timedelta from getopt import getopt, GetoptError from zope.security.interfaces import Unauthorized from zope.interface import implements from zope.server.buffers import OverflowableBuffer from zope.server.interfaces import ITask from zope.server.interfaces.ftp import IFileSystemAccess from zope.server.interfaces.ftp import IFTPCommandHandler from zope.server.linereceiver.lineserverchannel import LineServerChannel from zope.server.serverbase import ServerBase from zope.server.dualmodechannel import DualModeChannel, the_trigger status_messages = { 'OPEN_DATA_CONN' : '150 Opening %s mode data connection for file list', 'OPEN_CONN' : '150 Opening %s connection for %s', 'SUCCESS_200' : '200 %s command successful.', 'TYPE_SET_OK' : '200 Type set to %s.', 'STRU_OK' : '200 STRU F Ok.', 'MODE_OK' : '200 MODE S Ok.', 'FILE_DATE' : '213 %4d%02d%02d%02d%02d%02d', 'FILE_SIZE' : '213 %d Bytes', 'HELP_START' : '214-The following commands are recognized', 'HELP_END' : '214 Help done.', 'SERVER_TYPE' : '215 %s Type: %s', 'SERVER_READY' : '220 %s FTP server (Zope Async/Thread V0.1) ready.', 'GOODBYE' : '221 Goodbye.', 'SUCCESS_226' : '226 %s command successful.', 'TRANS_SUCCESS' : '226 Transfer successful.', 'PASV_MODE_MSG' : '227 Entering Passive Mode (%s,%d,%d)', 'LOGIN_SUCCESS' : '230 Login Successful.', 'SUCCESS_250' : '250 %s command successful.', 'SUCCESS_257' : '257 %s command successful.', 'ALREADY_CURRENT' : '257 "%s" is the current directory.', 'PASS_REQUIRED' : '331 Password required', 'RESTART_TRANSFER' : '350 Restarting at %d. Send STORE or ' 'RETRIEVE to initiate transfer.', 'READY_FOR_DEST' : '350 File exists, ready for destination.', 'NO_DATA_CONN' : "425 Can't build data connection", 'TRANSFER_ABORTED' : '426 Connection closed; transfer aborted.', 'CMD_UNKNOWN' : "500 '%s': command not understood.", 'INTERNAL_ERROR' : "500 Internal error: %s", 'ERR_ARGS' : '500 Bad command arguments', 'MODE_UNKOWN' : '502 Unimplemented MODE type', 'WRONG_BYTE_SIZE' : '504 Byte size must be 8', 'STRU_UNKNOWN' : '504 Unimplemented STRU type', 'NOT_AUTH' : "530 You are not authorized to perform the " "'%s' command", 'LOGIN_REQUIRED' : '530 Please log in with USER and PASS', 'LOGIN_MISMATCH' : '530 The username and password do not match.', 'ERR_NO_LIST' : '550 Could not list directory or file: %s', 'ERR_NO_DIR' : '550 "%s": No such directory.', 'ERR_NO_FILE' : '550 "%s": No such file.', 'ERR_NO_DIR_FILE' : '550 "%s": No such file or directory.', 'ERR_IS_NOT_FILE' : '550 "%s": Is not a file', 'ERR_CREATE_FILE' : '550 Error creating file.', 'ERR_CREATE_DIR' : '550 Error creating directory: %s', 'ERR_DELETE_FILE' : '550 Error deleting file: %s', 'ERR_DELETE_DIR' : '550 Error removing directory: %s', 'ERR_OPEN_READ' : '553 Could not open file for reading: %s', 'ERR_OPEN_WRITE' : '553 Could not open file for writing: %s', 'ERR_IO' : '553 I/O Error: %s', 'ERR_RENAME' : '560 Could not rename "%s" to "%s": %s', 'ERR_RNFR_SOURCE' : '560 No source filename specify. Call RNFR first.', } class FTPServerChannel(LineServerChannel): """The FTP Server Channel represents a connection to a particular client. We can therefore store information here.""" implements(IFTPCommandHandler) # List of commands that are always available special_commands = ( 'cmd_quit', 'cmd_type', 'cmd_noop', 'cmd_user', 'cmd_pass') # These are the commands that are accessing the filesystem. # Since this could be also potentially a longer process, these commands # are also the ones that are executed in a different thread. thread_commands = ( 'cmd_appe', 'cmd_cdup', 'cmd_cwd', 'cmd_dele', 'cmd_list', 'cmd_nlst', 'cmd_mdtm', 'cmd_mkd', 'cmd_pass', 'cmd_retr', 'cmd_rmd', 'cmd_rnfr', 'cmd_rnto', 'cmd_size', 'cmd_stor', 'cmd_stru') # Define the status messages status_messages = status_messages # Define the type of directory listing this server is returning system = ('UNIX', 'L8') # comply with (possibly troublesome) RFC959 requirements # This is necessary to correctly run an active data connection # through a firewall that triggers on the source port (expected # to be 'L-1', or 20 in the normal case). bind_local_minus_one = 0 restart_position = 0 type_map = {'a':'ASCII', 'i':'Binary', 'e':'EBCDIC', 'l':'Binary'} type_mode_map = {'a':'t', 'i':'b', 'e':'b', 'l':'b'} def __init__(self, server, conn, addr, adj=None): super(FTPServerChannel, self).__init__(server, conn, addr, adj) self.port_addr = None # The client's PORT address self.passive_listener = None # The PASV listener self.client_dc = None # The data connection self.transfer_mode = 'a' # Have to default to ASCII :-| self.passive_mode = 0 self.cwd = '/' self._rnfr = None self.username = '' self.credentials = None self.reply('SERVER_READY', self.server.server_name) def _getFileSystem(self): """Open the filesystem using the current credentials.""" return self.server.fs_access.open(self.credentials) def cmd_abor(self, args): 'See IFTPCommandHandler' assert self.async_mode self.reply('TRANSFER_ABORTED') self.abortPassive() self.abortData() def cmd_appe (self, args): 'See IFTPCommandHandler' return self.cmd_stor(args, 'a') def cmd_cdup(self, args): 'See IFTPCommandHandler' path = self._generatePath('../') if self._getFileSystem().type(path): self.cwd = path self.reply('SUCCESS_250', 'CDUP') else: self.reply('ERR_NO_FILE', path) def cmd_cwd(self, args): 'See IFTPCommandHandler' path = self._generatePath(args) if self._getFileSystem().type(path) == 'd': self.cwd = path self.reply('SUCCESS_250', 'CWD') else: self.reply('ERR_NO_DIR', path) def cmd_dele(self, args): 'See IFTPCommandHandler' if not args: self.reply('ERR_ARGS') return path = self._generatePath(args) try: self._getFileSystem().remove(path) except OSError, err: self.reply('ERR_DELETE_FILE', str(err)) else: self.reply('SUCCESS_250', 'DELE') def cmd_help(self, args): 'See IFTPCommandHandler' self.reply('HELP_START', flush=0) self.write('Help goes here somewhen.\r\n') self.reply('HELP_END') def cmd_list(self, args, long=1): 'See IFTPCommandHandler' opts = () if args.strip().startswith('-'): try: opts, args = getopt(args.split(), 'Llad') except GetoptError: self.reply('ERR_ARGS') return if len(args) > 1: self.reply('ERR_ARGS') return args = args and args[0] or '' fs = self._getFileSystem() path = self._generatePath(args) if not fs.type(path): self.reply('ERR_NO_DIR_FILE', path) return args = args.split() try: s = self.getList( args, long, directory=bool([opt for opt in opts if opt[0]=='-d']) ) except OSError, err: self.reply('ERR_NO_LIST', str(err)) return ok_reply = ('OPEN_DATA_CONN', self.type_map[self.transfer_mode]) cdc = RETRChannel(self, ok_reply) try: cdc.write(s) cdc.close_when_done() except OSError, err: self.reply('ERR_NO_LIST', str(err)) cdc.reported = True cdc.close_when_done() def getList(self, args, long=0, directory=0): # we need to scan the command line for arguments to '/bin/ls'... fs = self._getFileSystem() path_args = [] for arg in args: if arg[0] != '-': path_args.append (arg) else: # ignore arguments pass if len(path_args) < 1: path = '.' else: path = path_args[0] path = self._generatePath(path) if fs.type(path) == 'd' and not directory: if long: file_list = map(ls, fs.ls(path)) else: file_list = fs.names(path) else: if long: file_list = [ls(fs.lsinfo(path))] else: file_list = [posixpath.split(path)[1]] return '\r\n'.join(file_list) + '\r\n' def cmd_mdtm(self, args): 'See IFTPCommandHandler' fs = self._getFileSystem() # We simply do not understand this non-standard extension to MDTM if len(args.split()) > 1: self.reply('ERR_ARGS') return path = self._generatePath(args) if fs.type(path) != 'f': self.reply('ERR_IS_NOT_FILE', path) else: mtime = fs.mtime(path) if mtime is not None: mtime = (mtime.year, mtime.month, mtime.day, mtime.hour, mtime. minute, mtime.second) else: mtime = 0, 0, 0, 0, 0, 0 self.reply('FILE_DATE', mtime) def cmd_mkd(self, args): 'See IFTPCommandHandler' if not args: self.reply('ERR_ARGS') return path = self._generatePath(args) try: self._getFileSystem().mkdir(path) except OSError, err: self.reply('ERR_CREATE_DIR', str(err)) else: self.reply('SUCCESS_257', 'MKD') def cmd_mode(self, args): 'See IFTPCommandHandler' if len(args) == 1 and args in 'sS': self.reply('MODE_OK') else: self.reply('MODE_UNKNOWN') def cmd_nlst(self, args): 'See IFTPCommandHandler' self.cmd_list(args, 0) def cmd_noop(self, args): 'See IFTPCommandHandler' self.reply('SUCCESS_200', 'NOOP') def cmd_pass(self, args): 'See IFTPCommandHandler' self.authenticated = 0 password = args credentials = (self.username, password) try: self.server.fs_access.authenticate(credentials) except Unauthorized: self.reply('LOGIN_MISMATCH') self.close_when_done() else: self.credentials = credentials self.authenticated = 1 self.reply('LOGIN_SUCCESS') def cmd_pasv(self, args): 'See IFTPCommandHandler' assert self.async_mode # Kill any existing passive listener first. self.abortPassive() local_addr = self.getsockname()[0] self.passive_listener = PassiveListener(self, local_addr) port = self.passive_listener.port self.reply('PASV_MODE_MSG', (','.join(local_addr.split('.')), port/256, port%256 ) ) def cmd_port(self, args): 'See IFTPCommandHandler' info = args.split(',') ip = '.'.join(info[:4]) port = int(info[4])*256 + int(info[5]) # how many data connections at a time? # I'm assuming one for now... # TODO: we should (optionally) verify that the # ip number belongs to the client. [wu-ftpd does this?] self.port_addr = (ip, port) self.reply('SUCCESS_200', 'PORT') def cmd_pwd(self, args): 'See IFTPCommandHandler' self.reply('ALREADY_CURRENT', self.cwd) def cmd_quit(self, args): 'See IFTPCommandHandler' self.reply('GOODBYE') self.close_when_done() def cmd_retr(self, args): 'See IFTPCommandHandler' fs = self._getFileSystem() if not args: self.reply('CMD_UNKNOWN', 'RETR') path = self._generatePath(args) if not (fs.type(path) == 'f'): self.reply('ERR_IS_NOT_FILE', path) return start = 0 if self.restart_position: start = self.restart_position self.restart_position = 0 ok_reply = 'OPEN_CONN', (self.type_map[self.transfer_mode], path) cdc = RETRChannel(self, ok_reply) outstream = ApplicationOutputStream(cdc) try: fs.readfile(path, outstream, start) cdc.close_when_done() except OSError, err: self.reply('ERR_OPEN_READ', str(err)) cdc.reported = True cdc.close_when_done() except IOError, err: self.reply('ERR_IO', str(err)) cdc.reported = True cdc.close_when_done() def cmd_rest(self, args): 'See IFTPCommandHandler' try: pos = int(args) except ValueError: self.reply('ERR_ARGS') return self.restart_position = pos self.reply('RESTART_TRANSFER', pos) def cmd_rmd(self, args): 'See IFTPCommandHandler' if not args: self.reply('ERR_ARGS') return path = self._generatePath(args) try: self._getFileSystem().rmdir(path) except OSError, err: self.reply('ERR_DELETE_DIR', str(err)) else: self.reply('SUCCESS_250', 'RMD') def cmd_rnfr(self, args): 'See IFTPCommandHandler' path = self._generatePath(args) if self._getFileSystem().type(path): self._rnfr = path self.reply('READY_FOR_DEST') else: self.reply('ERR_NO_FILE', path) def cmd_rnto(self, args): 'See IFTPCommandHandler' path = self._generatePath(args) if self._rnfr is None: self.reply('ERR_RENAME') try: self._getFileSystem().rename(self._rnfr, path) except OSError, err: self.reply('ERR_RENAME', (self._rnfr, path, str(err))) else: self.reply('SUCCESS_250', 'RNTO') self._rnfr = None def cmd_size(self, args): 'See IFTPCommandHandler' path = self._generatePath(args) fs = self._getFileSystem() if fs.type(path) != 'f': self.reply('ERR_NO_FILE', path) else: self.reply('FILE_SIZE', fs.size(path)) def cmd_stor(self, args, write_mode='w'): 'See IFTPCommandHandler' if not args: self.reply('ERR_ARGS') return path = self._generatePath(args) start = 0 if self.restart_position: self.start = self.restart_position mode = write_mode + self.type_mode_map[self.transfer_mode] if not self._getFileSystem().writable(path): self.reply('ERR_OPEN_WRITE', "Can't write file") return cdc = STORChannel(self, (path, mode, start)) self.syncConnectData(cdc) self.reply('OPEN_CONN', (self.type_map[self.transfer_mode], path)) def finishSTOR(self, buffer, (path, mode, start)): """Called by STORChannel when the client has sent all data.""" assert not self.async_mode try: infile = buffer.getfile() infile.seek(0) self._getFileSystem().writefile(path, infile, start, append=(mode[0]=='a')) except OSError, err: self.reply('ERR_OPEN_WRITE', str(err)) except IOError, err: self.reply('ERR_IO', str(err)) except: self.exception() else: self.reply('TRANS_SUCCESS') def cmd_stru(self, args): 'See IFTPCommandHandler' if len(args) == 1 and args in 'fF': self.reply('STRU_OK') else: self.reply('STRU_UNKNOWN') def cmd_syst(self, args): 'See IFTPCommandHandler' self.reply('SERVER_TYPE', self.system) def cmd_type(self, args): 'See IFTPCommandHandler' # ascii, ebcdic, image, local args = args.split() t = args[0].lower() # no support for EBCDIC # if t not in ['a','e','i','l']: if t not in ['a','i','l']: self.reply('ERR_ARGS') elif t == 'l' and (len(args) > 2 and args[2] != '8'): self.reply('WRONG_BYTE_SIZE') else: self.transfer_mode = t self.reply('TYPE_SET_OK', self.type_map[t]) def cmd_user(self, args): 'See IFTPCommandHandler' self.authenticated = 0 if len(args) > 1: self.username = args self.reply('PASS_REQUIRED') else: self.reply('ERR_ARGS') ############################################################ def _generatePath(self, args): """Convert relative paths to absolute paths.""" # We use posixpath even on non-Posix platforms because we don't want # slashes converted to backslashes. path = posixpath.join(self.cwd, args) return posixpath.normpath(path) def syncConnectData(self, cdc): """Calls asyncConnectData in the asynchronous thread.""" the_trigger.pull_trigger(lambda: self.asyncConnectData(cdc)) def asyncConnectData(self, cdc): """Starts connecting the data channel. This is a little complicated because the data connection might be established already (in passive mode) or might be established in the near future (in port or passive mode.) If the connection has already been established, self.passive_listener already has a socket and is waiting for a call to connectData(). If the connection has not been established in passive mode, the passive listener will remember the data channel and send it when it's ready. In port mode, this method tells the data connection to connect. """ self.abortData() self.client_dc = cdc if self.passive_listener is not None: # Connect via PASV self.passive_listener.connectData(cdc) if self.port_addr: # Connect via PORT a = self.port_addr self.port_addr = None cdc.connectPort(a) def connectedPassive(self): """Accepted a passive connection.""" self.passive_listener = None def abortPassive(self): """Close the passive listener.""" if self.passive_listener is not None: self.passive_listener.abort() self.passive_listener = None def abortData(self): """Close the data connection.""" if self.client_dc is not None: self.client_dc.abort() self.client_dc = None def closedData(self): self.client_dc = None def close(self): # Make sure the passive listener and active client DC get closed. self.abortPassive() self.abortData() LineServerChannel.close(self) def ls(ls_info): """Formats a directory entry similarly to the 'ls' command. """ info = { 'owner_name': 'na', 'owner_readable': True, 'owner_writable': True, 'group_name': "na", 'group_readable': True, 'group_writable': True, 'other_readable': False, 'other_writable': False, 'nlinks': 1, 'size': 0, } if ls_info['type'] == 'd': info['owner_executable'] = True info['group_executable'] = True info['other_executable'] = True else: info['owner_executable'] = False info['group_executable'] = False info['other_executable'] = False info.update(ls_info) mtime = info.get('mtime') if mtime is not None: if date.today() - mtime.date() > timedelta(days=180): mtime = mtime.strftime('%b %d %Y') else: mtime = mtime.strftime('%b %d %H:%M') else: mtime = "Jan 02 0000" return "%s%s%s%s%s%s%s%s%s%s %3d %-8s %-8s %8d %s %s" % ( info['type'] == 'd' and 'd' or '-', info['owner_readable'] and 'r' or '-', info['owner_writable'] and 'w' or '-', info['owner_executable'] and 'x' or '-', info['group_readable'] and 'r' or '-', info['group_writable'] and 'w' or '-', info['group_executable'] and 'x' or '-', info['other_readable'] and 'r' or '-', info['other_writable'] and 'w' or '-', info['other_executable'] and 'x' or '-', info['nlinks'], info['owner_name'], info['group_name'], info['size'], mtime, info['name'], ) class PassiveListener(asyncore.dispatcher): """This socket accepts a data connection, used when the server has been placed in passive mode. Although the RFC implies that we ought to be able to use the same listener over and over again, this presents a problem: how do we shut it off, so that we are accepting connections only when we expect them? [we can't] wuftpd, and probably all the other servers, solve this by allowing only one connection to hit this listener. They then close it. Any subsequent data-connection command will then try for the default port on the client side [which is of course never there]. So the 'always-send-PORT/PASV' behavior seems required. Another note: wuftpd will also be listening on the channel as soon as the PASV command is sent. It does not wait for a data command first. """ def __init__ (self, control_channel, local_addr): asyncore.dispatcher.__init__ (self) self.control_channel = control_channel self.accepted = None # The accepted socket address self.client_dc = None # The data connection to accept the socket self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.closed = False # bind to an address on the interface where the # control connection is connected. self.bind((local_addr, 0)) self.port = self.getsockname()[1] self.listen(1) def log (self, *ignore): pass def abort(self): """Abort the passive listener.""" if not self.closed: self.closed = True self.close() if self.accepted is not None: self.accepted.close() def handle_accept (self): """Accept a connection from the client. For some reason, sometimes accept() returns None instead of a socket. This code ignores that case. """ v = self.accept() if v is None: return self.accepted, addr = v if self.accepted is None: return self.accepted.setblocking(0) self.closed = True self.close() if self.client_dc is not None: self.connectData(self.client_dc) def connectData(self, cdc): """Sends the connection to the data channel. If the connection has not yet been made, sends the connection when it becomes available. """ if self.accepted is not None: cdc.set_socket(self.accepted) # Note that this method will be called twice, once by the # control channel, and once by handle_accept, and the two # calls may come in either order. If handle_accept calls # first, we don't want to call set_socket() on the data # connection twice, so set self.accepted = None to keep a # record that the data connection already has the socket. self.accepted = None self.control_channel.connectedPassive() else: self.client_dc = cdc class FTPDataChannel(DualModeChannel): """Base class for FTP data connections. Note that data channels are always in async mode. """ def __init__ (self, control_channel): self.control_channel = control_channel self.reported = False self.closed = False DualModeChannel.__init__(self, None, None, control_channel.adj) def connectPort(self, client_addr): """Connect to a port on the client""" self.create_socket(socket.AF_INET, socket.SOCK_STREAM) #if bind_local_minus_one: # self.bind(('', self.control_channel.server.port - 1)) try: self.connect(client_addr) except socket.error: self.report('NO_DATA_CONN') def abort(self): """Abort the data connection without reporting.""" self.reported = True if not self.closed: self.closed = True self.close() def report(self, *reply_args): """Reports the result of the data transfer.""" self.reported = True if self.control_channel is not None: self.control_channel.reply(*reply_args) def reportDefault(self): """Provide a default report on close.""" pass def close(self): """Notifies the control channel when the data connection closes.""" c = self.control_channel try: if c is not None and c.connected and not self.reported: self.reportDefault() finally: self.control_channel = None DualModeChannel.close(self) if c is not None: c.closedData() class STORChannel(FTPDataChannel): """Channel for uploading one file from client to server""" complete_transfer = 0 _fileno = None # provide a default for asyncore.dispatcher._fileno def __init__ (self, control_channel, finish_args): self.finish_args = finish_args self.inbuf = OverflowableBuffer(control_channel.adj.inbuf_overflow) FTPDataChannel.__init__(self, control_channel) # Note that this channel starts in async mode. def writable (self): return 0 def handle_connect (self): pass def received (self, data): if data: self.inbuf.append(data) def handle_close (self): """Client closed, indicating EOF.""" c = self.control_channel task = FinishSTORTask(c, self.inbuf, self.finish_args) self.complete_transfer = 1 self.close() c.queue_task(task) def reportDefault(self): if not self.complete_transfer: self.report('TRANSFER_ABORTED') # else the transfer completed and FinishSTORTask will # provide a complete reply through finishSTOR(). class FinishSTORTask(object): """Calls control_channel.finishSTOR() in an application thread. This task executes after the client has finished uploading. """ implements(ITask) def __init__(self, control_channel, inbuf, finish_args): self.control_channel = control_channel self.inbuf = inbuf self.finish_args = finish_args def service(self): """Called to execute the task. """ close_on_finish = 0 c = self.control_channel try: try: c.finishSTOR(self.inbuf, self.finish_args) except socket.error: close_on_finish = 1 if c.adj.log_socket_errors: raise finally: if close_on_finish: c.close_when_done() def cancel(self): 'See ITask' self.control_channel.close_when_done() def defer(self): 'See ITask' pass class RETRChannel(FTPDataChannel): """Channel for downloading one file from server to client Also used for directory listings. """ opened = 0 _fileno = None # provide a default for asyncore.dispatcher._fileno def __init__ (self, control_channel, ok_reply_args): self.ok_reply_args = ok_reply_args FTPDataChannel.__init__(self, control_channel) def _open(self): """Signal the client to open the connection.""" self.opened = 1 self.control_channel.reply(*self.ok_reply_args) self.control_channel.asyncConnectData(self) def write(self, data): if self.control_channel is None: raise IOError('Client FTP connection closed') if not self.opened: self._open() return FTPDataChannel.write(self, data) def readable(self): return not self.connected def handle_read(self): # This may be called upon making the connection. try: self.recv(1) except socket.error: # The connection failed. self.report('NO_DATA_CONN') self.close() def handle_connect(self): pass def handle_comm_error(self): self.report('TRANSFER_ABORTED') self.close() def reportDefault(self): if not len(self.outbuf): # All data transferred if not self.opened: # Zero-length file self._open() self.report('TRANS_SUCCESS') else: # Not all data transferred self.report('TRANSFER_ABORTED') class ApplicationOutputStream(object): """Provide stream output to RETRChannel. Maps close() to close_when_done(). """ def __init__(self, retr_channel): self.write = retr_channel.write self.flush = retr_channel.flush self.close = retr_channel.close_when_done class FTPServer(ServerBase): """Generic FTP Server""" channel_class = FTPServerChannel SERVER_IDENT = 'zope.server.ftp' def __init__(self, ip, port, fs_access, *args, **kw): assert IFileSystemAccess.providedBy(fs_access) self.fs_access = fs_access super(FTPServer, self).__init__(ip, port, *args, **kw) zope.server-3.8.6/src/zope/server/ftp/tests/0000775000175000017500000000000011701734350020052 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/ftp/tests/test_publisher.py0000664000175000017500000000657611701734340023475 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2003 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Test the FTP publisher. """ import demofs from unittest import TestCase, TestSuite, main, makeSuite from fstests import FileSystemTests from StringIO import StringIO from zope.publisher.publish import mapply class DemoFileSystem(demofs.DemoFileSystem): def rename(self, path, old, new): return demofs.DemoFileSystem.rename( self, "%s/%s" % (path, old), "%s/%s" % (path, new)) class Publication(object): def __init__(self, root): self.root = root def beforeTraversal(self, request): pass def getApplication(self, request): return self.root def afterTraversal(self, request, ob): pass def callObject(self, request, ob): command = getattr(ob, request.env['command']) if 'name' in request.env: request.env['path'] += "/" + request.env['name'] return mapply(command, request = request.env) def afterCall(self, request, ob): pass def endRequest(self, request, ob): pass def handleException(self, object, request, info, retry_allowed=True): request.response._exc = info[:2] class Request(object): def __init__(self, input, env): self.env = env self.response = Response() self.user = env['credentials'] del env['credentials'] def processInputs(self): pass def traverse(self, root): root.user = self.user return root def close(self): pass class Response(object): _exc = _body = None def setResult(self, result): self._result = result def getResult(self): if self._exc: raise self._exc[0], self._exc[1] return self._result class RequestFactory(object): def __init__(self, root): self.pub = Publication(root) def __call__(self, input, env): r = Request(input, env) r.publication = self.pub return r class TestPublisherFileSystem(FileSystemTests, TestCase): def setUp(self): root = demofs.Directory() root.grant('bob', demofs.write) fs = DemoFileSystem(root, 'bob') fs.mkdir(self.dir_name) fs.writefile(self.file_name, StringIO(self.file_contents)) fs.writefile(self.unwritable_filename, StringIO("save this")) fs.get(self.unwritable_filename).revoke('bob', demofs.write) # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.publisher import PublisherFileSystem self.filesystem = PublisherFileSystem('bob', RequestFactory(fs)) def test_suite(): return TestSuite(( makeSuite(TestPublisherFileSystem), )) if __name__=='__main__': main(defaultTest='test_suite') zope.server-3.8.6/src/zope/server/ftp/tests/test_ftpserver.py0000664000175000017500000003455711701734340023520 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """FTP Server tests """ import asyncore import ftplib import socket import sys import time import traceback import unittest from types import StringType from StringIO import StringIO from threading import Thread, Event from zope.server.adjustments import Adjustments from zope.server.ftp.tests import demofs from zope.server.taskthreads import ThreadedTaskDispatcher from zope.server.tests.asyncerror import AsyncoreErrorHook td = ThreadedTaskDispatcher() LOCALHOST = '127.0.0.1' SERVER_PORT = 0 # Set these port numbers to 0 to auto-bind, or CONNECT_TO_PORT = 0 # use specific numbers to inspect using TCPWatch. my_adj = Adjustments() def retrlines(ftpconn, cmd): res = [] ftpconn.retrlines(cmd, res.append) return ''.join(res) class Tests(unittest.TestCase, AsyncoreErrorHook): def setUp(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import FTPServer td.setThreadCount(1) if len(asyncore.socket_map) != 1: # Let sockets die off. # TODO tests should be more careful to clear the socket map. asyncore.poll(0.1) self.orig_map_size = len(asyncore.socket_map) self.hook_asyncore_error() root_dir = demofs.Directory() root_dir['test'] = demofs.Directory() root_dir['test'].access['foo'] = 7 root_dir['private'] = demofs.Directory() root_dir['private'].access['foo'] = 7 root_dir['private'].access['anonymous'] = 0 fs = demofs.DemoFileSystem(root_dir, 'foo') fs.writefile('/test/existing', StringIO('test initial data')) fs.writefile('/private/existing', StringIO('private initial data')) self.__fs = fs = demofs.DemoFileSystem(root_dir, 'root') fs.writefile('/existing', StringIO('root initial data')) fs_access = demofs.DemoFileSystemAccess(root_dir, {'foo': 'bar'}) self.server = FTPServer(LOCALHOST, SERVER_PORT, fs_access, task_dispatcher=td, adj=my_adj) if CONNECT_TO_PORT == 0: self.port = self.server.socket.getsockname()[1] else: self.port = CONNECT_TO_PORT self.run_loop = 1 self.counter = 0 self.thread_started = Event() self.thread = Thread(target=self.loop) self.thread.setDaemon(True) self.thread.start() self.thread_started.wait(10.0) self.assert_(self.thread_started.isSet()) def tearDown(self): self.run_loop = 0 self.thread.join() td.shutdown() self.server.close() # Make sure all sockets get closed by asyncore normally. timeout = time.time() + 2 while 1: if len(asyncore.socket_map) == self.orig_map_size: # Clean! break if time.time() >= timeout: self.fail('Leaked a socket: %s' % `asyncore.socket_map`) break asyncore.poll(0.1) self.unhook_asyncore_error() def loop(self): self.thread_started.set() import select from errno import EBADF while self.run_loop: self.counter = self.counter + 1 # Note that it isn't acceptable to fail out of # this loop. That will likely make the tests hang. try: asyncore.poll(0.1) continue except select.error, data: print "EXCEPTION POLLING IN LOOP(): ", data if data[0] == EBADF: for key in asyncore.socket_map.keys(): print try: select.select([], [], [key], 0.0) except select.error, v: print "Bad entry in socket map", key, v print asyncore.socket_map[key] print asyncore.socket_map[key].__class__ del asyncore.socket_map[key] else: print "OK entry in socket map", key print asyncore.socket_map[key] print asyncore.socket_map[key].__class__ print except: print "WEIRD EXCEPTION IN LOOP" traceback.print_exception(*(sys.exc_info()+(100,))) print def getFTPConnection(self, login=1): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages ftp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ftp.connect((LOCALHOST, self.port)) result = ftp.recv(10000).split()[0] self.assertEqual(result, '220') if login: ftp.send('USER foo\r\n') self.assertEqual(ftp.recv(1024), status_messages['PASS_REQUIRED'] +'\r\n') ftp.send('PASS bar\r\n') self.assertEqual(ftp.recv(1024), status_messages['LOGIN_SUCCESS'] +'\r\n') return ftp def execute(self, commands, login=1): ftp = self.getFTPConnection(login) try: if type(commands) is StringType: commands = (commands,) for command in commands: ftp.send('%s\r\n' %command) result = ftp.recv(10000) self.failUnless(result.endswith('\r\n')) finally: ftp.close() return result def testABOR(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('ABOR', 1).rstrip(), status_messages['TRANSFER_ABORTED']) def testAPPE(self): conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('foo', 'bar') fp = StringIO('Charity never faileth') # Successful write conn.storbinary('APPE /test/existing', fp) self.assertEqual(self.__fs.files['test']['existing'].data, 'test initial dataCharity never faileth') finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testAPPE_errors(self): conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('foo', 'bar') fp = StringIO('Speak softly') # Can't overwrite directory self.assertRaises( ftplib.error_perm, conn.storbinary, 'APPE /test', fp) # No such file self.assertRaises( ftplib.error_perm, conn.storbinary, 'APPE /nosush', fp) # No such dir self.assertRaises( ftplib.error_perm, conn.storbinary, 'APPE /nosush/f', fp) # Not allowed self.assertRaises( ftplib.error_perm, conn.storbinary, 'APPE /existing', fp) finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testCDUP(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.execute('CWD test', 1) self.assertEqual(self.execute('CDUP', 1).rstrip(), status_messages['SUCCESS_250'] %'CDUP') self.assertEqual(self.execute('CDUP', 1).rstrip(), status_messages['SUCCESS_250'] %'CDUP') def testCWD(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('CWD test', 1).rstrip(), status_messages['SUCCESS_250'] %'CWD') self.assertEqual(self.execute('CWD foo', 1).rstrip(), status_messages['ERR_NO_DIR'] %'/foo') def testDELE(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('DELE test/existing', 1).rstrip(), status_messages['SUCCESS_250'] %'DELE') res = self.execute('DELE bar', 1).split()[0] self.assertEqual(res, '550') self.assertEqual(self.execute('DELE', 1).rstrip(), status_messages['ERR_ARGS']) def testHELP(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages result = status_messages['HELP_START'] + '\r\n' result += 'Help goes here somewhen.\r\n' result += status_messages['HELP_END'] + '\r\n' self.assertEqual(self.execute('HELP', 1), result) def testLIST(self): conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('anonymous', 'bar') self.assertRaises(ftplib.Error, retrlines, conn, 'LIST /foo') listing = retrlines(conn, 'LIST') self.assert_(len(listing) > 0) listing = retrlines(conn, 'LIST -la') self.assert_(len(listing) > 0) finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testMKDLIST(self): self.execute(['MKD test/f1', 'MKD test/f2'], 1) conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('foo', 'bar') listing = [] conn.retrlines('LIST /test', listing.append) self.assert_(len(listing) > 2) listing = [] conn.retrlines('LIST -lad test/f1', listing.append) self.assertEqual(len(listing), 1) self.assertEqual(listing[0][0], 'd') finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testNOOP(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('NOOP', 0).rstrip(), status_messages['SUCCESS_200'] %'NOOP') self.assertEqual(self.execute('NOOP', 1).rstrip(), status_messages['SUCCESS_200'] %'NOOP') def testPASS(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('PASS', 0).rstrip(), status_messages['LOGIN_MISMATCH']) self.execute('USER blah', 0) self.assertEqual(self.execute('PASS bar', 0).rstrip(), status_messages['LOGIN_MISMATCH']) def testQUIT(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('QUIT', 0).rstrip(), status_messages['GOODBYE']) self.assertEqual(self.execute('QUIT', 1).rstrip(), status_messages['GOODBYE']) def testSTOR(self): conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('foo', 'bar') fp = StringIO('Speak softly') # Can't overwrite directory self.assertRaises( ftplib.error_perm, conn.storbinary, 'STOR /test', fp) fp = StringIO('Charity never faileth') # Successful write conn.storbinary('STOR /test/stuff', fp) self.assertEqual(self.__fs.files['test']['stuff'].data, 'Charity never faileth') finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testSTOR_over(self): conn = ftplib.FTP() try: conn.connect(LOCALHOST, self.port) conn.login('foo', 'bar') fp = StringIO('Charity never faileth') conn.storbinary('STOR /test/existing', fp) self.assertEqual(self.__fs.files['test']['existing'].data, 'Charity never faileth') finally: conn.close() # Make sure no garbage was left behind. self.testNOOP() def testUSER(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.ftp.server import status_messages self.assertEqual(self.execute('USER foo', 0).rstrip(), status_messages['PASS_REQUIRED']) self.assertEqual(self.execute('USER', 0).rstrip(), status_messages['ERR_ARGS']) def test_suite(): loader = unittest.TestLoader() return loader.loadTestsFromTestCase(Tests) if __name__=='__main__': unittest.TextTestRunner().run(test_suite()) zope.server-3.8.6/src/zope/server/ftp/tests/test_demofs.py0000664000175000017500000000257111701734340022744 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2003 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Test the Demo Filesystem implementation. """ import demofs from unittest import TestCase, TestSuite, main, makeSuite from fstests import FileSystemTests from StringIO import StringIO class Test(FileSystemTests, TestCase): def setUp(self): root = demofs.Directory() root.grant('bob', demofs.write) fs = self.filesystem = demofs.DemoFileSystem(root, 'bob') fs.mkdir(self.dir_name) fs.writefile(self.file_name, StringIO(self.file_contents)) fs.writefile(self.unwritable_filename, StringIO("save this")) fs.get(self.unwritable_filename).revoke('bob', demofs.write) def test_suite(): return TestSuite(( makeSuite(Test), )) if __name__=='__main__': main(defaultTest='test_suite') zope.server-3.8.6/src/zope/server/ftp/tests/demofs.py0000664000175000017500000002175711701734340021714 0ustar mgmg00000000000000############################################################################## # Copyright (c) 2003 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. ############################################################################## """Demo file-system implementation, for testing """ import posixpath from zope.security.interfaces import Unauthorized from zope.server.interfaces.ftp import IFileSystem from zope.server.interfaces.ftp import IFileSystemAccess from zope.interface import implements execute = 1 read = 2 write = 4 class File(object): type = 'f' modified=None def __init__(self): self.access = {'anonymous': read} def accessable(self, user, access=read): return (user == 'root' or (self.access.get(user, 0) & access) or (self.access.get('anonymous', 0) & access) ) def grant(self, user, access): self.access[user] = self.access.get(user, 0) | access def revoke(self, user, access): self.access[user] = self.access.get(user, 0) ^ access class Directory(File): type = 'd' def __init__(self): super(Directory, self).__init__() self.files = {} def get(self, name, default=None): return self.files.get(name, default) def __getitem__(self, name): return self.files[name] def __setitem__(self, name, v): self.files[name] = v def __delitem__(self, name): del self.files[name] def __contains__(self, name): return name in self.files def __iter__(self): return iter(self.files) class DemoFileSystem(object): __doc__ = IFileSystem.__doc__ implements(IFileSystem) File = File Directory = Directory def __init__(self, files, user=''): self.files = files self.user = user def get(self, path, default=None): while path.startswith('/'): path = path[1:] d = self.files if path: for name in path.split('/'): if d.type is not 'd': return default if not d.accessable(self.user): raise Unauthorized d = d.get(name) if d is None: break return d def getany(self, path): d = self.get(path) if d is None: raise OSError("No such file or directory:", path) return d def getdir(self, path): d = self.getany(path) if d.type != 'd': raise OSError("Not a directory:", path) return d def getfile(self, path): d = self.getany(path) if d.type != 'f': raise OSError("Not a file:", path) return d def getwdir(self, path): d = self.getdir(path) if not d.accessable(self.user, write): raise OSError("Permission denied") return d def type(self, path): "See zope.server.interfaces.ftp.IFileSystem" f = self.get(path) return getattr(f, 'type', None) def names(self, path, filter=None): "See zope.server.interfaces.ftp.IFileSystem" f = list(self.getdir(path)) if filter is not None: f = [name for name in f if filter(name)] return f def _lsinfo(self, name, file): info = { 'type': file.type, 'name': name, 'group_read': file.accessable(self.user, read), 'group_write': file.accessable(self.user, write), } if file.type == 'f': info['size'] = len(file.data) if file.modified is not None: info['mtime'] = file.modified return info def ls(self, path, filter=None): "See zope.server.interfaces.ftp.IFileSystem" f = self.getdir(path) if filter is None: return [self._lsinfo(name, f.files[name]) for name in f ] return [self._lsinfo(name, f.files[name]) for name in f if filter(name)] def readfile(self, path, outstream, start=0, end=None): "See zope.server.interfaces.ftp.IFileSystem" f = self.getfile(path) data = f.data if end is not None: data = data[:end] if start: data = data[start:] outstream.write(data) def lsinfo(self, path): "See zope.server.interfaces.ftp.IFileSystem" f = self.getany(path) return self._lsinfo(posixpath.split(path)[1], f) def mtime(self, path): "See zope.server.interfaces.ftp.IFileSystem" f = self.getany(path) return f.modified def size(self, path): "See zope.server.interfaces.ftp.IFileSystem" f = self.getany(path) return len(getattr(f, 'data', '')) def mkdir(self, path): "See zope.server.interfaces.ftp.IFileSystem" path, name = posixpath.split(path) d = self.getwdir(path) if name in d.files: raise OSError("Already exists:", name) newdir = self.Directory() newdir.grant(self.user, read | write) d.files[name] = newdir def remove(self, path): "See zope.server.interfaces.ftp.IFileSystem" path, name = posixpath.split(path) d = self.getwdir(path) if name not in d.files: raise OSError("Not exists:", name) f = d.files[name] if f.type == 'd': raise OSError('Is a directory:', name) del d.files[name] def rmdir(self, path): "See zope.server.interfaces.ftp.IFileSystem" path, name = posixpath.split(path) d = self.getwdir(path) if name not in d.files: raise OSError("Not exists:", name) f = d.files[name] if f.type != 'd': raise OSError('Is not a directory:', name) del d.files[name] def rename(self, old, new): "See zope.server.interfaces.ftp.IFileSystem" oldpath, oldname = posixpath.split(old) newpath, newname = posixpath.split(new) olddir = self.getwdir(oldpath) newdir = self.getwdir(newpath) if oldname not in olddir.files: raise OSError("Not exists:", oldname) if newname in newdir.files: raise OSError("Already exists:", newname) newdir.files[newname] = olddir.files[oldname] del olddir.files[oldname] def writefile(self, path, instream, start=None, end=None, append=False): "See zope.server.interfaces.ftp.IFileSystem" path, name = posixpath.split(path) d = self.getdir(path) f = d.files.get(name) if f is None: d = self.getwdir(path) f = d.files[name] = self.File() f.grant(self.user, read | write) elif f.type != 'f': raise OSError("Can't overwrite a directory") if not f.accessable(self.user, write): raise OSError("Permission denied") if append: f.data += instream.read() else: if start: if start < 0: raise ValueError("Negative starting file position") prefix = f.data[:start] if len(prefix) < start: prefix += '\0' * (start - len(prefix)) else: prefix = '' start=0 if end: if end < 0: raise ValueError("Negative ending file position") l = end - start newdata = instream.read(l) f.data = prefix+newdata+f.data[start+len(newdata):] else: f.data = prefix + instream.read() def writable(self, path): "See zope.server.interfaces.ftp.IFileSystem" path, name = posixpath.split(path) try: d = self.getdir(path) except OSError: return False if name not in d: return d.accessable(self.user, write) f = d[name] return f.type == 'f' and f.accessable(self.user, write) class DemoFileSystemAccess(object): __doc__ = IFileSystemAccess.__doc__ implements(IFileSystemAccess) def __init__(self, files, users): self.files = files self.users = users def authenticate(self, credentials): "See zope.server.interfaces.ftp.IFileSystemAccess" user, password = credentials if user != 'anonymous': if self.users.get(user) != password: raise Unauthorized return user def open(self, credentials): "See zope.server.interfaces.ftp.IFileSystemAccess" user = self.authenticate(credentials) return DemoFileSystem(self.files, user) zope.server-3.8.6/src/zope/server/ftp/tests/__init__.py0000664000175000017500000000004111701734340022155 0ustar mgmg00000000000000# Make this directory a package. zope.server-3.8.6/src/zope/server/ftp/tests/fstests.py0000664000175000017500000001170511701734340022122 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Abstract file-system tests """ from StringIO import StringIO from zope.interface.verify import verifyObject from zope.server.interfaces.ftp import IFileSystem class FileSystemTests(object): """Tests of a readable filesystem """ filesystem = None dir_name = '/dir' file_name = '/dir/file.txt' unwritable_filename = '/dir/protected.txt' dir_contents = ['file.txt', 'protected.txt'] file_contents = 'Lengthen your stride' def test_type(self): self.assertEqual(self.filesystem.type(self.dir_name), 'd') self.assertEqual(self.filesystem.type(self.file_name), 'f') def test_names(self): lst = self.filesystem.names(self.dir_name) lst.sort() self.assertEqual(lst, self.dir_contents) def test_readfile(self): s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), self.file_contents) def testReadPartOfFile(self): s = StringIO() self.filesystem.readfile(self.file_name, s, 2) self.assertEqual(s.getvalue(), self.file_contents[2:]) def testReadPartOfFile2(self): s = StringIO() self.filesystem.readfile(self.file_name, s, 1, 5) self.assertEqual(s.getvalue(), self.file_contents[1:5]) def test_IFileSystemInterface(self): verifyObject(IFileSystem, self.filesystem) def testRemove(self): self.filesystem.remove(self.file_name) self.failIf(self.filesystem.type(self.file_name)) def testMkdir(self): path = self.dir_name + '/x' self.filesystem.mkdir(path) self.assertEqual(self.filesystem.type(path), 'd') def testRmdir(self): self.filesystem.remove(self.file_name) self.filesystem.rmdir(self.dir_name) self.failIf(self.filesystem.type(self.dir_name)) def testRename(self): self.filesystem.rename(self.file_name, self.file_name + '.bak') self.assertEqual(self.filesystem.type(self.file_name), None) self.assertEqual(self.filesystem.type(self.file_name + '.bak'), 'f') def testWriteFile(self): s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), self.file_contents) data = 'Always ' + self.file_contents s = StringIO(data) self.filesystem.writefile(self.file_name, s) s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), data) def testAppendToFile(self): data = ' again' s = StringIO(data) self.filesystem.writefile(self.file_name, s, append=True) s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), self.file_contents + data) def testWritePartOfFile(self): data = '123' s = StringIO(data) self.filesystem.writefile(self.file_name, s, 3, 6) expect = self.file_contents[:3] + data + self.file_contents[6:] s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), expect) def testWritePartOfFile_and_truncate(self): data = '123' s = StringIO(data) self.filesystem.writefile(self.file_name, s, 3) expect = self.file_contents[:3] + data s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), expect) def testWriteBeyondEndOfFile(self): partlen = len(self.file_contents) - 6 data = 'daylight savings' s = StringIO(data) self.filesystem.writefile(self.file_name, s, partlen) expect = self.file_contents[:partlen] + data s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), expect) def testWriteNewFile(self): s = StringIO(self.file_contents) self.filesystem.writefile(self.file_name + '.new', s) s = StringIO() self.filesystem.readfile(self.file_name, s) self.assertEqual(s.getvalue(), self.file_contents) def test_writable(self): self.failIf(self.filesystem.writable(self.dir_name)) self.failIf(self.filesystem.writable(self.unwritable_filename)) self.failUnless(self.filesystem.writable(self.file_name)) self.failUnless(self.filesystem.writable(self.file_name+'1')) zope.server-3.8.6/src/zope/server/ftp/README.txt0000664000175000017500000000063111701734341020406 0ustar mgmg00000000000000FTP Framework This file contains documentation on the FTP server framework. The core server is implemented in server.py. This relies on a file-system abstraction, defined in zope.server.interfaces.py. The publisher module provides the connection to the object publsihing system by providing a file-system implementation that delegates file-system operations to objects through the publisher. zope.server-3.8.6/src/zope/server/ftp/logger.py0000664000175000017500000000250011701734341020536 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Common FTP Activity Logger """ import time from zope.server.http.commonaccesslogger import CommonAccessLogger class CommonFTPActivityLogger(CommonAccessLogger): """Outputs hits in common HTTP log format.""" def log(self, task): """Receives a completed task and logs it in the common log format.""" now = time.time() message = ' - %s [%s] "%s %s"' % (task.channel.username, self.log_date_string(now), task.m_name[4:].upper(), task.channel.cwd, ) self.output.logRequest(task.channel.addr[0], message) zope.server-3.8.6/src/zope/server/ftp/publisher.py0000664000175000017500000001136211701734341021262 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Zope Publisher-based FTP Server This FTP server uses the Zope 3 Publisher to execute commands. """ import posixpath from cStringIO import StringIO from zope.server.interfaces.ftp import IFileSystem from zope.server.interfaces.ftp import IFileSystemAccess from zope.server.ftp.server import FTPServer from zope.publisher.publish import publish from zope.interface import implements class PublisherFileSystem(object): """Generic Publisher FileSystem implementation.""" implements(IFileSystem) def __init__ (self, credentials, request_factory): self.credentials = credentials self.request_factory = request_factory def type(self, path): if path == '/': return 'd' return self._execute(path, 'type') def names(self, path, filter=None): return self._execute(path, 'names', split=False, filter=filter) def ls(self, path, filter=None): return self._execute(path, 'ls', split=False, filter=filter) def readfile(self, path, outstream, start=0, end=None): return self._execute(path, 'readfile', outstream=outstream, start=start, end=end) def lsinfo(self, path): return self._execute(path, 'lsinfo') def mtime(self, path): return self._execute(path, 'mtime') def size(self, path): return self._execute(path, 'size') def mkdir(self, path): return self._execute(path, 'mkdir') def remove(self, path): return self._execute(path, 'remove') def rmdir(self, path): return self._execute(path, 'rmdir') def rename(self, old, new): 'See IWriteFileSystem' old = self._translate(old) new = self._translate(new) path0, old = posixpath.split(old) path1, new = posixpath.split(new) assert path0 == path1 return self._execute(path0, 'rename', split=False, old=old, new=new) def writefile(self, path, instream, start=None, end=None, append=False): 'See IWriteFileSystem' return self._execute( path, 'writefile', instream=instream, start=start, end=end, append=append) def writable(self, path): 'See IWriteFileSystem' return self._execute(path, 'writable') def _execute(self, path, command, split=True, **kw): env = {} env.update(kw) env['command'] = command path = self._translate(path) if split: env['path'], env['name'] = posixpath.split(path) else: env['path'] = path env['credentials'] = self.credentials request = self.request_factory(StringIO(''), env) # Note that publish() calls close() on request, which deletes the # response from the request, so that we need to keep track of it. # agroszer: 2008.feb.1.: currently the above seems not to be true # request will KEEP the response on close() # even more if a retry occurs in the publisher, # the response will be LOST, so we must accept the returned request request = publish(request) return request.response.getResult() def _translate (self, path): # Normalize path = posixpath.normpath(path) if path.startswith('..'): # Someone is trying to get lower than the permitted root. # We just ignore it. path = '/' return path class PublisherFTPServer(FTPServer): """Generic FTP Server""" def __init__(self, request_factory, name, ip, port, *args, **kw): fs_access = PublisherFileSystemAccess(request_factory) super(PublisherFTPServer, self).__init__(ip, port, fs_access, *args, **kw) class PublisherFileSystemAccess(object): implements(IFileSystemAccess) def __init__(self, request_factory): self.request_factory = request_factory def authenticate(self, credentials): # We can't actually do any authentication initially, as the # user may not be defined at the root. pass def open(self, credentials): return PublisherFileSystem(credentials, self.request_factory) zope.server-3.8.6/src/zope/server/ftp/__init__.py0000664000175000017500000000007511701734341021023 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/utilities.py0000664000175000017500000000213111701734341020501 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Server utility functions """ def find_double_newline(s): """Returns the position just after a double newline in the given string.""" pos1 = s.find('\n\r\n') # One kind of double newline if pos1 >= 0: pos1 += 3 pos2 = s.find('\n\n') # Another kind of double newline if pos2 >= 0: pos2 += 2 if pos1 >= 0: if pos2 >= 0: return min(pos1, pos2) else: return pos1 else: return pos2 zope.server-3.8.6/src/zope/server/tests/0000775000175000017500000000000011701734350017261 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/tests/test_serverbase.py0000664000175000017500000000722011701734341023034 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Tests for zope.server.serverbase """ import doctest import unittest def doctest_ServerBase(): r"""Regression test for ServerBase Bug: if the `ip` argument of ServerBase is a string containing a numberic IP address, and the verbose argument is enabled, ServerBase.__init__ would try to use self.logger before it was initialized. We will use a subclass of ServerBase so that unit tests do not actually try to bind to ports. >>> from zope.server.serverbase import ServerBase >>> class ServerBaseForTest(ServerBase): ... def bind(self, (ip, port)): ... print "Listening on %s:%d" % (ip or '*', port) >>> sb = ServerBaseForTest('127.0.0.1', 80, start=False, verbose=True) Listening on 127.0.0.1:80 """ def doctest_ServerBase_startup_logging(): r"""Test for ServerBase verbose startup logging We will use a subclass of ServerBase so that unit tests do not actually try to bind to ports. >>> from zope.server.serverbase import ServerBase >>> class ServerBaseForTest(ServerBase): ... def bind(self, (ip, port)): ... self.socket = FakeSocket() ... def log_info(self, message, level='info'): ... print message.expandtabs() >>> sb = ServerBaseForTest('example.com', 80, start=True, verbose=True) zope.server.serverbase started. Hostname: example.com Port: 80 Subclasses can add extra information there >>> class ServerForTest(ServerBaseForTest): ... def getExtraLogMessage(self): ... return '\n\tURL: http://example.com/' >>> sb = ServerForTest('example.com', 80, start=True, verbose=True) zope.server.serverbase started. Hostname: example.com Port: 80 URL: http://example.com/ """ class FakeSocket: data = '' setblocking = lambda *_: None fileno = lambda *_: 42 getpeername = lambda *_: ('localhost', 42) def listen(self, *args): pass def send(self, data): self.data += data return len(data) def channels_accept_iterables(): r""" Channels accept iterables (they special-case strings). >>> from zope.server.dualmodechannel import DualModeChannel >>> socket = FakeSocket() >>> channel = DualModeChannel(socket, ('localhost', 42)) >>> channel.write("First") 5 >>> channel.flush() >>> print socket.data First >>> channel.write(["\n", "Second", "\n", "Third"]) 13 >>> channel.flush() >>> print socket.data First Second Third >>> def count(): ... yield '\n1\n2\n3\n' ... yield 'I love to count. Ha ha ha.' >>> channel.write(count()) 33 >>> channel.flush() >>> print socket.data First Second Third 1 2 3 I love to count. Ha ha ha. """ def test_suite(): return doctest.DocTestSuite() if __name__ == '__main__': unittest.main(defaultTest='test_suite') zope.server-3.8.6/src/zope/server/tests/test_zombies.py0000664000175000017500000001024511701734341022344 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Tests for zope.server.serverchannelbase zombie logic """ import doctest import unittest class FakeSocket: data = '' setblocking = lambda *_: None close = lambda *_: None def __init__(self, no): self.no = no def fileno(self): return self.no def getpeername(self): return ('localhost', self.no) def send(self, data): self.data += data return len(data) def recv(self, data): return 'data' def zombies_test(): """Regression test for ServerChannelBase kill_zombies method Bug: This method checks for channels that have been "inactive" for a configured time. The bug was that last_activity is set at creation time but never updated during async channel activity (reads and writes), so any channel older than the configured timeout will be closed when a new channel is created, regardless of activity. >>> import time >>> import zope.server.adjustments >>> config = zope.server.adjustments.Adjustments() >>> from zope.server.serverbase import ServerBase >>> class ServerBaseForTest(ServerBase): ... def bind(self, (ip, port)): ... print "Listening on %s:%d" % (ip or '*', port) >>> sb = ServerBaseForTest('127.0.0.1', 80, start=False, verbose=True) Listening on 127.0.0.1:80 First we confirm the correct behavior, where a channel with no activity for the timeout duration gets closed. >>> from zope.server.serverchannelbase import ServerChannelBase >>> socket = FakeSocket(42) >>> channel = ServerChannelBase(sb, socket, ('localhost', 42)) >>> channel.connected True >>> channel.last_activity -= int(config.channel_timeout) + 1 >>> channel.next_channel_cleanup[0] = channel.creation_time - int( ... config.cleanup_interval) - 1 >>> socket2 = FakeSocket(7) >>> channel2 = ServerChannelBase(sb, socket2, ('localhost', 7)) >>> channel.connected False Write Activity -------------- Now we make sure that if there is activity the channel doesn't get closed incorrectly. >>> channel2.connected True >>> channel2.last_activity -= int(config.channel_timeout) + 1 >>> channel2.handle_write() >>> channel2.next_channel_cleanup[0] = channel2.creation_time - int( ... config.cleanup_interval) - 1 >>> socket3 = FakeSocket(3) >>> channel3 = ServerChannelBase(sb, socket3, ('localhost', 3)) >>> channel2.connected True Read Activity -------------- We should test to see that read activity will update a channel as well. >>> channel3.connected True >>> channel3.last_activity -= int(config.channel_timeout) + 1 >>> import zope.server.http.httprequestparser >>> channel3.parser_class = ( ... zope.server.http.httprequestparser.HTTPRequestParser) >>> channel3.handle_read() >>> channel3.next_channel_cleanup[0] = channel3.creation_time - int( ... config.cleanup_interval) - 1 >>> socket4 = FakeSocket(4) >>> channel4 = ServerChannelBase(sb, socket4, ('localhost', 4)) >>> channel3.connected True Main loop window ---------------- There is also a corner case we'll do a shallow test for where a channel can be closed waiting for the main loop. >>> channel4.last_activity -= 1 >>> last_active = channel4.last_activity >>> channel4.set_async() >>> channel4.last_activity != last_active True """ def test_suite(): return doctest.DocTestSuite() if __name__ == '__main__': unittest.main(defaultTest='test_suite') zope.server-3.8.6/src/zope/server/tests/asyncerror.py0000664000175000017500000000355411701734341022031 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Mixin class to turn uncaught asyncore errors into test failues. By default, asyncore handles uncaught exceptions in dispatchers by printing a message to the console. If a test causes such uncaught exceptions, the test is marked as a failure, because asyncore handles the exception. This framework causes the unit test to fail. If code being tested expects the errors to occur, it can add code to prevent the error from propagating all the way back to asyncore. """ import asyncore import sys import traceback class AsyncoreErrorHook(object): """Convert asyncore errors into unittest failures. Call hook_asyncore_error in setUp() and unhook_asyncore_error() in tearDown(), or use super() to call setUp() and tearDown() here. """ def setUp(self): self.hook_asyncore_error() def tearDown(self): self.unhook_asycnore_error() def hook_asyncore_error(self): self._asyncore_traceback = asyncore.compact_traceback asyncore.compact_traceback = self.handle_asyncore_error def unhook_asyncore_error(self): asyncore.compact_traceback = self._asyncore_traceback def handle_asyncore_error(self): L = traceback.format_exception(*sys.exc_info()) self.fail("".join(L)) zope.server-3.8.6/src/zope/server/tests/__init__.py0000664000175000017500000000007511701734341021374 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/http/0000775000175000017500000000000011701734350017076 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/http/httpserver.py0000664000175000017500000000346111701734336021666 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Server This server uses asyncore to accept connections and do initial processing but threads to do work. """ from zope.server.serverbase import ServerBase from zope.server.http.httpserverchannel import HTTPServerChannel class HTTPServer(ServerBase): """This is a generic HTTP Server.""" channel_class = HTTPServerChannel SERVER_IDENT = 'zope.server.http' def executeRequest(self, task): """Execute an HTTP request.""" # This is a default implementation, meant to be overridden. body = "The HTTP server is running!\r\n" * 10 task.response_headers['Content-Type'] = 'text/plain' task.response_headers['Content-Length'] = str(len(body)) task.write(body) def getExtraLogMessage(self): return '\n\tURL: http://%s:%d/' % (self.server_name, self.port) if __name__ == '__main__': from zope.server.taskthreads import ThreadedTaskDispatcher td = ThreadedTaskDispatcher() td.setThreadCount(4) HTTPServer('', 8080, task_dispatcher=td) try: import asyncore while 1: asyncore.poll(5) except KeyboardInterrupt: print 'shutting down...' td.shutdown() zope.server-3.8.6/src/zope/server/http/httptask.py0000664000175000017500000002077311701734336021327 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Task An HTTP task that can execute an HTTP request with the help of the channel and the server it belongs to. """ import socket import time from zope.server.http.http_date import build_http_date from zope.publisher.interfaces.http import IHeaderOutput from zope.server.interfaces import ITask from zope.interface import implements rename_headers = { 'CONTENT_LENGTH' : 'CONTENT_LENGTH', 'CONTENT_TYPE' : 'CONTENT_TYPE', 'CONNECTION' : 'CONNECTION_TYPE', } class HTTPTask(object): """An HTTP task accepts a request and writes to a channel. Subclass this and override the execute() method. """ implements(ITask, IHeaderOutput) #, IOutputStream instream = None close_on_finish = 1 status = '200' reason = 'OK' wrote_header = 0 accumulated_headers = None bytes_written = 0 auth_user_name = '' cgi_env = None def __init__(self, channel, request_data): self.channel = channel self.request_data = request_data self.response_headers = {} version = request_data.version if version not in ('1.0', '1.1'): # fall back to a version we support. version = '1.0' self.version = version def service(self): """See zope.server.interfaces.ITask""" try: try: self.start() self.channel.server.executeRequest(self) self.finish() except socket.error: self.close_on_finish = 1 if self.channel.adj.log_socket_errors: raise finally: if self.close_on_finish: self.channel.close_when_done() def cancel(self): """See zope.server.interfaces.ITask""" self.channel.close_when_done() def defer(self): """See zope.server.interfaces.ITask""" pass def setResponseStatus(self, status, reason): """See zope.publisher.interfaces.http.IHeaderOutput""" self.status = status self.reason = reason def setResponseHeaders(self, mapping): """See zope.publisher.interfaces.http.IHeaderOutput""" self.response_headers.update(mapping) def appendResponseHeaders(self, lst): """See zope.publisher.interfaces.http.IHeaderOutput""" accum = self.accumulated_headers if accum is None: self.accumulated_headers = accum = [] accum.extend(lst) def wroteResponseHeader(self): """See zope.publisher.interfaces.http.IHeaderOutput""" return self.wrote_header def setAuthUserName(self, name): """See zope.publisher.interfaces.http.IHeaderOutput""" self.auth_user_name = name def prepareResponseHeaders(self): version = self.version # Figure out whether the connection should be closed. connection = self.request_data.headers.get('CONNECTION', '').lower() close_it = 0 response_headers = self.response_headers accumulated_headers = self.accumulated_headers if accumulated_headers is None: accumulated_headers = [] if version == '1.0': if connection == 'keep-alive': if not ('Content-Length' in response_headers): close_it = 1 else: response_headers['Connection'] = 'Keep-Alive' else: close_it = 1 elif version == '1.1': if 'connection: close' in (header.lower() for header in accumulated_headers): close_it = 1 if connection == 'close': close_it = 1 elif 'Transfer-Encoding' in response_headers: if not response_headers['Transfer-Encoding'] == 'chunked': close_it = 1 elif self.status == '304': # Replying with headers only. pass elif not ('Content-Length' in response_headers): # accumulated_headers is a simple list, we need to cut off # the value of content-length manually if 'content-length' not in (header[:14].lower() for header in accumulated_headers): close_it = 1 # under HTTP 1.1 keep-alive is default, no need to set the header else: # Close if unrecognized HTTP version. close_it = 1 self.close_on_finish = close_it if close_it: self.response_headers['Connection'] = 'close' # Set the Server and Date field, if not yet specified. This is needed # if the server is used as a proxy. if 'server' not in (header[:6].lower() for header in accumulated_headers): self.response_headers['Server'] = self.channel.server.SERVER_IDENT else: self.response_headers['Via'] = self.channel.server.SERVER_IDENT if 'date' not in (header[:4].lower() for header in accumulated_headers): self.response_headers['Date'] = build_http_date(self.start_time) def buildResponseHeader(self): self.prepareResponseHeaders() first_line = 'HTTP/%s %s %s' % (self.version, self.status, self.reason) lines = [first_line] + ['%s: %s' % hv for hv in self.response_headers.items()] accum = self.accumulated_headers if accum is not None: lines.extend(accum) res = '%s\r\n\r\n' % '\r\n'.join(lines) return res def getCGIEnvironment(self): """Returns a CGI-like environment.""" env = self.cgi_env if env is not None: # Return the cached copy. return env request_data = self.request_data path = request_data.path channel = self.channel server = channel.server while path and path.startswith('/'): path = path[1:] env = {} env['REQUEST_METHOD'] = request_data.command.upper() env['SERVER_PORT'] = str(server.port) env['SERVER_NAME'] = server.server_name env['SERVER_SOFTWARE'] = server.SERVER_IDENT env['SERVER_PROTOCOL'] = "HTTP/%s" % self.version env['CHANNEL_CREATION_TIME'] = channel.creation_time env['SCRIPT_NAME']='' env['PATH_INFO']='/' + path query = request_data.query if query: env['QUERY_STRING'] = query env['GATEWAY_INTERFACE'] = 'CGI/1.1' addr = channel.addr[0] env['REMOTE_ADDR'] = addr # If the server has a resolver, try to get the # remote host from the resolver's cache. resolver = getattr(server, 'resolver', None) if resolver is not None: dns_cache = resolver.cache if addr in dns_cache: remote_host = dns_cache[addr][2] if remote_host is not None: env['REMOTE_HOST'] = remote_host env_has = env.has_key for key, value in request_data.headers.items(): value = value.strip() mykey = rename_headers.get(key, None) if mykey is None: mykey = 'HTTP_%s' % key if not env_has(mykey): env[mykey] = value self.cgi_env = env return env def start(self): now = time.time() self.start_time = now def finish(self): if not self.wrote_header: self.write('') hit_log = self.channel.server.hit_log if hit_log is not None: hit_log.log(self) def write(self, data): channel = self.channel if not self.wrote_header: rh = self.buildResponseHeader() channel.write(rh) self.bytes_written += len(rh) self.wrote_header = 1 if data: self.bytes_written += channel.write(data) def flush(self): self.channel.flush() zope.server-3.8.6/src/zope/server/http/commonaccesslogger.py0000664000175000017500000000610211701734336023325 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Common Access Logger """ import time from zope.server.http.http_date import monthname from zope.server.logger.pythonlogger import PythonLogger from zope.server.logger.resolvinglogger import ResolvingLogger from zope.server.logger.unresolvinglogger import UnresolvingLogger class CommonAccessLogger(object): """Outputs accesses in common HTTP log format. """ def __init__(self, logger_object=None, resolver=None): if logger_object is None: # logger_object is an IMessageLogger logger_object = PythonLogger('accesslog') # self.output is an IRequestLogger if resolver is not None: self.output = ResolvingLogger(resolver, logger_object) else: self.output = UnresolvingLogger(logger_object) def compute_timezone_for_log(self, tz): if tz > 0: neg = 1 else: neg = 0 tz = -tz h, rem = divmod (tz, 3600) m, rem = divmod (rem, 60) if neg: return '-%02d%02d' % (h, m) else: return '+%02d%02d' % (h, m) tz_for_log = None tz_for_log_alt = None def log_date_string(self, when): logtime = time.localtime(when) Y, M, D, h, m, s = logtime[:6] if not time.daylight: tz = self.tz_for_log if tz is None: tz = self.compute_timezone_for_log(time.timezone) self.tz_for_log = tz else: tz = self.tz_for_log_alt if tz is None: tz = self.compute_timezone_for_log(time.altzone) self.tz_for_log_alt = tz return '%d/%s/%02d:%02d:%02d:%02d %s' % ( D, monthname[M], Y, h, m, s, tz) def log(self, task): """Receives a completed task and logs it in the common log format.""" now = time.time() request_data = task.request_data req_headers = request_data.headers user_name = task.auth_user_name or 'anonymous' user_agent = req_headers.get('USER_AGENT', '') referer = req_headers.get('REFERER', '') self.output.logRequest( task.channel.addr[0], ' - %s [%s] "%s" %s %d "%s" "%s"\n' % ( user_name, self.log_date_string(now), request_data.first_line, task.status, task.bytes_written, referer, user_agent ) ) zope.server-3.8.6/src/zope/server/http/http_date.py0000664000175000017500000001002311701734336021424 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Server Date/Time utilities """ import re import string import time import calendar def concat(*args): return ''.join(args) def join(seq, field=' '): return field.join(seq) def group(s): return '(' + s + ')' short_days = ['sun','mon','tue','wed','thu','fri','sat'] long_days = ['sunday','monday','tuesday','wednesday', 'thursday','friday','saturday'] short_day_reg = group(join(short_days, '|')) long_day_reg = group(join(long_days, '|')) daymap = {} for i in range(7): daymap[short_days[i]] = i daymap[long_days[i]] = i hms_reg = join(3 * [group('[0-9][0-9]')], ':') months = ['jan','feb','mar','apr','may','jun','jul', 'aug','sep','oct','nov','dec'] monmap = {} for i in range(12): monmap[months[i]] = i+1 months_reg = group(join(months, '|')) # From draft-ietf-http-v11-spec-07.txt/3.3.1 # Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 # Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 # Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format # rfc822 format rfc822_date = join( [concat (short_day_reg,','), # day group('[0-9][0-9]?'), # date months_reg, # month group('[0-9]+'), # year hms_reg, # hour minute second 'gmt' ], ' ' ) rfc822_reg = re.compile(rfc822_date) def unpack_rfc822(m): g = m.group a = string.atoi return ( a(g(4)), # year monmap[g(3)], # month a(g(2)), # day a(g(5)), # hour a(g(6)), # minute a(g(7)), # second 0, 0, 0 ) # rfc850 format rfc850_date = join( [concat(long_day_reg,','), join( [group ('[0-9][0-9]?'), months_reg, group ('[0-9]+') ], '-' ), hms_reg, 'gmt' ], ' ' ) rfc850_reg = re.compile(rfc850_date) # they actually unpack the same way def unpack_rfc850(m): g = m.group a = string.atoi return ( a(g(4)), # year monmap[g(3)], # month a(g(2)), # day a(g(5)), # hour a(g(6)), # minute a(g(7)), # second 0, 0, 0 ) # parsdate.parsedate - ~700/sec. # parse_http_date - ~1333/sec. weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] def build_http_date(when): year, month, day, hh, mm, ss, wd, y, z = time.gmtime(when) return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( weekdayname[wd], day, monthname[month], year, hh, mm, ss) def parse_http_date(d): d = d.lower() m = rfc850_reg.match(d) if m and m.end() == len(d): retval = int(calendar.timegm(unpack_rfc850(m))) else: m = rfc822_reg.match(d) if m and m.end() == len(d): retval = int(calendar.timegm(unpack_rfc822(m))) else: return 0 return retval zope.server-3.8.6/src/zope/server/http/publisherhttpserver.py0000664000175000017500000000452611701734336023607 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Server that uses the Zope Publisher for executing a task. """ from zope.server.http import wsgihttpserver from zope.publisher.publish import publish import zope.security.management class PublisherHTTPServer(wsgihttpserver.WSGIHTTPServer): def __init__(self, request_factory, sub_protocol=None, *args, **kw): def application(environ, start_response): request = request_factory(environ['wsgi.input'], environ) request = publish(request) response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBody() return super(PublisherHTTPServer, self).__init__( application, sub_protocol, *args, **kw) class PMDBHTTPServer(wsgihttpserver.WSGIHTTPServer): def __init__(self, request_factory, sub_protocol=None, *args, **kw): def application(environ, start_response): request = request_factory(environ['wsgi.input'], environ) try: request = publish(request, handle_errors=False) except: import sys, pdb print "%s:" % sys.exc_info()[0] print sys.exc_info()[1] zope.security.management.restoreInteraction() try: pdb.post_mortem(sys.exc_info()[2]) raise finally: zope.security.management.endInteraction() response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBody() return super(PublisherHTTPServer, self).__init__( application, sub_protocol, *args, **kw) zope.server-3.8.6/src/zope/server/http/chunking.py0000664000175000017500000000717011701734336021267 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Data Chunk Receiver """ from zope.server.utilities import find_double_newline from zope.server.interfaces import IStreamConsumer from zope.interface import implements class ChunkedReceiver(object): implements(IStreamConsumer) chunk_remainder = 0 control_line = '' all_chunks_received = 0 trailer = '' completed = 0 # max_control_line = 1024 # max_trailer = 65536 def __init__(self, buf): self.buf = buf def received(self, s): # Returns the number of bytes consumed. if self.completed: return 0 orig_size = len(s) while s: rm = self.chunk_remainder if rm > 0: # Receive the remainder of a chunk. to_write = s[:rm] self.buf.append(to_write) written = len(to_write) s = s[written:] self.chunk_remainder -= written elif not self.all_chunks_received: # Receive a control line. s = self.control_line + s pos = s.find('\n') if pos < 0: # Control line not finished. self.control_line = s s = '' else: # Control line finished. line = s[:pos] s = s[pos + 1:] self.control_line = '' line = line.strip() if line: # Begin a new chunk. semi = line.find(';') if semi >= 0: # discard extension info. line = line[:semi] sz = int(line.strip(), 16) # hexadecimal if sz > 0: # Start a new chunk. self.chunk_remainder = sz else: # Finished chunks. self.all_chunks_received = 1 # else expect a control line. else: # Receive the trailer. trailer = self.trailer + s if trailer.startswith('\r\n'): # No trailer. self.completed = 1 return orig_size - (len(trailer) - 2) elif trailer.startswith('\n'): # No trailer. self.completed = 1 return orig_size - (len(trailer) - 1) pos = find_double_newline(trailer) if pos < 0: # Trailer not finished. self.trailer = trailer s = '' else: # Finished the trailer. self.completed = 1 self.trailer = trailer[:pos] return orig_size - (len(trailer) - pos) return orig_size def getfile(self): return self.buf.getfile() zope.server-3.8.6/src/zope/server/http/tests/0000775000175000017500000000000011701734350020240 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/http/tests/test_httprequestparser.py0000664000175000017500000001065611701734336025472 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Request Parser tests """ import unittest from zope.server.http.httprequestparser import HTTPRequestParser from zope.server.adjustments import Adjustments my_adj = Adjustments() class Tests(unittest.TestCase): def setUp(self): self.parser = HTTPRequestParser(my_adj) def feed(self, data): parser = self.parser for n in xrange(100): # make sure we never loop forever consumed = parser.received(data) data = data[consumed:] if parser.completed: return raise ValueError('Looping') def testSimpleGET(self): data = """\ GET /foobar HTTP/8.4 FirstName: mickey lastname: Mouse content-length: 7 Hello. """ parser = self.parser self.feed(data) self.failUnless(parser.completed) self.assertEqual(parser.version, '8.4') self.failIf(parser.empty) self.assertEqual(parser.headers, {'FIRSTNAME': 'mickey', 'LASTNAME': 'Mouse', 'CONTENT_LENGTH': '7', }) self.assertEqual(parser.path, '/foobar') self.assertEqual(parser.command, 'GET') self.assertEqual(parser.query, None) self.assertEqual(parser.proxy_scheme, '') self.assertEqual(parser.proxy_netloc, '') self.assertEqual(parser.getBodyStream().getvalue(), 'Hello.\n') def testComplexGET(self): data = """\ GET /foo/a+%2B%2F%C3%A4%3D%26a%3Aint?d=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6 HTTP/8.4 FirstName: mickey lastname: Mouse content-length: 10 Hello mickey. """ parser = self.parser self.feed(data) self.assertEqual(parser.command, 'GET') self.assertEqual(parser.version, '8.4') self.failIf(parser.empty) self.assertEqual(parser.headers, {'FIRSTNAME': 'mickey', 'LASTNAME': 'Mouse', 'CONTENT_LENGTH': '10', }) # path should be utf-8 encoded self.assertEqual(parser.path, '/foo/a++/\xc3\xa4=&a:int') self.assertEqual(parser.query, 'd=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6') self.assertEqual(parser.getBodyStream().getvalue(), 'Hello mick') def testProxyGET(self): data = """\ GET https://example.com:8080/foobar HTTP/8.4 content-length: 7 Hello. """ parser = self.parser self.feed(data) self.failUnless(parser.completed) self.assertEqual(parser.version, '8.4') self.failIf(parser.empty) self.assertEqual(parser.headers, {'CONTENT_LENGTH': '7', }) self.assertEqual(parser.path, '/foobar') self.assertEqual(parser.command, 'GET') self.assertEqual(parser.proxy_scheme, 'https') self.assertEqual(parser.proxy_netloc, 'example.com:8080') self.assertEqual(parser.command, 'GET') self.assertEqual(parser.query, None) self.assertEqual(parser.getBodyStream().getvalue(), 'Hello.\n') def testDuplicateHeaders(self): # Ensure that headers with the same key get concatenated as per # RFC2616. data = """\ GET /foobar HTTP/8.4 x-forwarded-for: 10.11.12.13 x-forwarded-for: unknown,127.0.0.1 X-Forwarded_for: 255.255.255.255 content-length: 7 Hello. """ self.feed(data) self.failUnless(self.parser.completed) self.assertEqual(self.parser.headers, { 'CONTENT_LENGTH': '7', 'X_FORWARDED_FOR': '10.11.12.13, unknown,127.0.0.1, 255.255.255.255', }) def test_suite(): loader = unittest.TestLoader() return loader.loadTestsFromTestCase(Tests) if __name__=='__main__': unittest.TextTestRunner().run(test_suite()) zope.server-3.8.6/src/zope/server/http/tests/test_wsgiserver.py0000664000175000017500000004211111701734336024054 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001 Zope Foundation and Contributors. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. ############################################################################## """Test Publisher-based HTTP Server """ import StringIO import sys import unittest from asyncore import socket_map, poll from threading import Thread from time import sleep from httplib import HTTPConnection from zope.server.taskthreads import ThreadedTaskDispatcher from zope.component.testing import PlacelessSetup import zope.component from zope.i18n.interfaces import IUserPreferredCharsets from zope.publisher.publish import publish from zope.publisher.http import IHTTPRequest from zope.publisher.http import HTTPCharsets from zope.publisher.browser import BrowserRequest from zope.publisher.base import DefaultPublication from zope.publisher.interfaces import Redirect, Retry from zope.publisher.http import HTTPRequest td = ThreadedTaskDispatcher() LOCALHOST = '127.0.0.1' HTTPRequest.STAGGER_RETRIES = 0 # Don't pause. class Conflict(Exception): """ Pseudo ZODB conflict error. """ ERROR_RESPONSE = "error occurred" RESPONSE = "normal response" class DummyException(Exception): value = "Dummy Exception to test start_response" def __str__(self): return repr(self.value) class PublicationWithConflict(DefaultPublication): def handleException(self, object, request, exc_info, retry_allowed=1): if exc_info[0] is Conflict and retry_allowed: # This simulates a ZODB retry. raise Retry(exc_info) else: DefaultPublication.handleException(self, object, request, exc_info, retry_allowed) class Accepted(Exception): pass class tested_object(object): """Docstring required by publisher.""" tries = 0 def __call__(self, REQUEST): return 'URL invoked: %s' % REQUEST.URL def redirect_method(self, REQUEST): "Generates a redirect using the redirect() method." REQUEST.response.redirect("/redirect") def redirect_exception(self): "Generates a redirect using an exception." raise Redirect("/exception") def conflict(self, REQUEST, wait_tries): """ Returns 202 status only after (wait_tries) tries. """ if self.tries >= int(wait_tries): raise Accepted else: self.tries += 1 raise Conflict def proxy(self, REQUEST): """Behaves like a real proxy response.""" REQUEST.response.addHeader('Server', 'Fake/1.0') REQUEST.response.addHeader('Date', 'Thu, 01 Apr 2010 12:00:00 GMT') return 'Proxied Content' class WSGIInfo(object): """Docstring required by publisher""" def __call__(self, REQUEST): """Return a list of variables beginning with 'wsgi.'""" r = [] for name in REQUEST.keys(): if name.startswith('wsgi.'): r.append(name) return ' '.join(r) def version(self, REQUEST): """Return WSGI version""" return str(REQUEST['wsgi.version']) def url_scheme(self, REQUEST): """Return WSGI URL scheme""" return REQUEST['wsgi.url_scheme'] def multithread(self, REQUEST): """Return WSGI multithreadedness""" return str(bool(REQUEST['wsgi.multithread'])) def multiprocess(self, REQUEST): """Return WSGI multiprocessedness""" return str(bool(REQUEST['wsgi.multiprocess'])) def run_once(self, REQUEST): """Return whether WSGI app is invoked only once or not""" return str(bool(REQUEST['wsgi.run_once'])) def proxy_scheme(self, REQUEST): """Return the proxy scheme.""" return REQUEST['zserver.proxy.scheme'] def proxy_host(self, REQUEST): """Return the proxy host.""" return REQUEST['zserver.proxy.host'] class Tests(PlacelessSetup, unittest.TestCase): def _getServerClass(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.http.wsgihttpserver import WSGIHTTPServer return WSGIHTTPServer def setUp(self): super(Tests, self).setUp() zope.component.provideAdapter(HTTPCharsets, [IHTTPRequest], IUserPreferredCharsets, '') obj = tested_object() obj.folder = tested_object() obj.folder.item = tested_object() obj._protected = tested_object() obj.wsgi = WSGIInfo() pub = PublicationWithConflict(obj) def application(environ, start_response): request = BrowserRequest(environ['wsgi.input'], environ) request.setPublication(pub) request = publish(request) response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBodyIter() td.setThreadCount(4) # Bind to any port on localhost. ServerClass = self._getServerClass() self.server = ServerClass(application, 'Browser', LOCALHOST, 0, task_dispatcher=td) self.port = self.server.socket.getsockname()[1] self.run_loop = 1 self.thread = Thread(target=self.loop) self.thread.start() sleep(0.1) # Give the thread some time to start. def tearDown(self): self.run_loop = 0 self.thread.join() td.shutdown() self.server.close() super(Tests, self).tearDown() def loop(self): while self.run_loop: poll(0.1, socket_map) def invokeRequest(self, path='/', add_headers=None, request_body='', return_response=False): h = HTTPConnection(LOCALHOST, self.port) h.putrequest('GET', path) h.putheader('Accept', 'text/plain') if add_headers: for k, v in add_headers.items(): h.putheader(k, v) if request_body: h.putheader('Content-Length', str(int(len(request_body)))) h.endheaders() if request_body: h.send(request_body) response = h.getresponse() if return_response: return response length = int(response.getheader('Content-Length', '0')) if length: response_body = response.read(length) else: response_body = '' self.assertEqual(length, len(response_body)) return response.status, response_body def testDeeperPath(self): status, response_body = self.invokeRequest('/folder/item') self.assertEqual(status, 200) expect_response = 'URL invoked: http://%s:%d/folder/item' % ( LOCALHOST, self.port) self.assertEqual(response_body, expect_response) def testNotFound(self): status, response_body = self.invokeRequest('/foo/bar') self.assertEqual(status, 404) def testUnauthorized(self): status, response_body = self.invokeRequest('/_protected') self.assertEqual(status, 401) def testRedirectMethod(self): status, response_body = self.invokeRequest('/redirect_method') self.assertEqual(status, 303) def testRedirectException(self): status, response_body = self.invokeRequest('/redirect_exception') self.assertEqual(status, 303) status, response_body = self.invokeRequest('/folder/redirect_exception') self.assertEqual(status, 303) def testConflictRetry(self): status, response_body = self.invokeRequest('/conflict?wait_tries=2') # Expect the "Accepted" response since the retries will succeed. self.assertEqual(status, 202) def testFailedConflictRetry(self): status, response_body = self.invokeRequest('/conflict?wait_tries=10') # Expect a "Conflict" response since there will be too many # conflicts. self.assertEqual(status, 409) def testServerAsProxy(self): response = self.invokeRequest( '/proxy', return_response=True) # The headers set by the proxy are honored, self.assertEqual( response.getheader('Server'), 'Fake/1.0') self.assertEqual( response.getheader('Date'), 'Thu, 01 Apr 2010 12:00:00 GMT') # The server adds a Via header. self.assertEqual( response.getheader('Via'), 'zope.server.http (Browser)') # And the content got here too. self.assertEqual(response.read(), 'Proxied Content') def testWSGIVariables(self): # Assert that the environment contains all required WSGI variables status, response_body = self.invokeRequest('/wsgi') wsgi_variables = set(response_body.split()) self.assertEqual(wsgi_variables, set(['wsgi.version', 'wsgi.url_scheme', 'wsgi.input', 'wsgi.errors', 'wsgi.multithread', 'wsgi.multiprocess', 'wsgi.run_once'])) def testWSGIVersion(self): status, response_body = self.invokeRequest('/wsgi/version') self.assertEqual("(1, 0)", response_body) def testWSGIURLScheme(self): status, response_body = self.invokeRequest('/wsgi/url_scheme') self.assertEqual('http', response_body) def testWSGIMultithread(self): status, response_body = self.invokeRequest('/wsgi/multithread') self.assertEqual('True', response_body) def testWSGIMultiprocess(self): status, response_body = self.invokeRequest('/wsgi/multiprocess') self.assertEqual('True', response_body) def testWSGIRunOnce(self): status, response_body = self.invokeRequest('/wsgi/run_once') self.assertEqual('False', response_body) def testWSGIProxy(self): status, response_body = self.invokeRequest( 'https://zope.org:8080/wsgi/proxy_scheme') self.assertEqual('https', response_body) status, response_body = self.invokeRequest( 'https://zope.org:8080/wsgi/proxy_host') self.assertEqual('zope.org:8080', response_body) def test_ensure_multiple_task_write_calls(self): # In order to get data out as fast as possible, the WSGI server needs # to call task.write() multiple times. orig_app = self.server.application def app(eviron, start_response): start_response('200 Ok', []) return ['This', 'is', 'my', 'response.'] self.server.application = app class FakeTask: wrote_header = 0 counter = 0 getCGIEnvironment = lambda _: {} class request_data: getBodyStream = lambda _: StringIO.StringIO() request_data = request_data() setResponseStatus = appendResponseHeaders = lambda *_: None def wroteResponseHeader(self): return self.wrote_header def write(self, v): self.counter += 1 task = FakeTask() self.server.executeRequest(task) self.assertEqual(task.counter, 4) self.server.application = orig_app def _getFakeAppAndTask(self): def app(environ, start_response): try: raise DummyException() except DummyException: start_response( '500 Internal Error', [('Content-type', 'text/plain')], sys.exc_info()) return ERROR_RESPONSE.split() return RESPONSE.split() class FakeTask: wrote_header = 0 status = None reason = None response = [] accumulated_headers = None def __init__(self): self.accumulated_headers = [] self.response_headers = {} getCGIEnvironment = lambda _: {} class request_data: getBodyStream = lambda _: StringIO.StringIO() request_data = request_data() def appendResponseHeaders(self, lst): accum = self.accumulated_headers if accum is None: self.accumulated_headers = accum = [] accum.extend(lst) def setResponseStatus(self, status, reason): self.status = status self.reason = reason def wroteResponseHeader(self): return self.wrote_header def write(self, v): self.response.append(v) return app, FakeTask() def test_start_response_with_no_headers_sent(self): # start_response exc_info if no headers have been sent orig_app = self.server.application self.server.application, task = self._getFakeAppAndTask() task.accumulated_headers = ['header1', 'header2'] task.accumulated_headers = {'key1': 'value1', 'key2': 'value2'} self.server.executeRequest(task) self.assertEqual(task.status, "500") self.assertEqual(task.response, ERROR_RESPONSE.split()) # any headers written before are cleared and # only the most recent one is added. self.assertEqual(task.accumulated_headers, ['Content-type: text/plain']) # response headers are cleared. They'll be rebuilt from # accumulated_headers in the prepareResponseHeaders method self.assertEqual(task.response_headers, {}) self.server.application = orig_app def test_multiple_start_response_calls(self): # if start_response is called more than once with no exc_info ignore, task = self._getFakeAppAndTask() task.wrote_header = 1 self.assertRaises(AssertionError, self.server.executeRequest, task) def test_start_response_with_headers_sent(self): # If headers have been sent it raises the exception orig_app = self.server.application self.server.application, task = self._getFakeAppAndTask() # If headers have already been written an exception is raised task.wrote_header = 1 self.assertRaises(DummyException, self.server.executeRequest, task) self.server.application = orig_app class PMDBTests(Tests): def _getServerClass(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.http.wsgihttpserver import PMDBWSGIHTTPServer return PMDBWSGIHTTPServer def testWSGIVariables(self): # Assert that the environment contains all required WSGI variables status, response_body = self.invokeRequest('/wsgi') wsgi_variables = set(response_body.split()) self.assertEqual(wsgi_variables, set(['wsgi.version', 'wsgi.url_scheme', 'wsgi.input', 'wsgi.errors', 'wsgi.multithread', 'wsgi.multiprocess', 'wsgi.handleErrors', 'wsgi.run_once'])) def test_multiple_start_response_calls(self): # if start_response is called more than once with no exc_info ignore, task = self._getFakeAppAndTask() task.wrote_header = 1 # monkey-patch pdb.post_mortem so we don't go into pdb session. pm_traceback = [] def fake_post_mortem(tb): import traceback pm_traceback.extend(traceback.format_tb(tb)) import pdb orig_post_mortem = pdb.post_mortem pdb.post_mortem = fake_post_mortem self.assertRaises(AssertionError, self.server.executeRequest, task) expected_msg = "start_response called a second time" self.assertTrue(expected_msg in pm_traceback[-1]) pdb.post_mortem = orig_post_mortem def test_start_response_with_headers_sent(self): # If headers have been sent it raises the exception, which will # be caught by the server and invoke pdb.post_mortem. orig_app = self.server.application self.server.application, task = self._getFakeAppAndTask() task.wrote_header = 1 # monkey-patch pdb.post_mortem so we don't go into pdb session. pm_traceback = [] def fake_post_mortem(tb): import traceback pm_traceback.extend(traceback.format_tb(tb)) import pdb orig_post_mortem = pdb.post_mortem pdb.post_mortem = fake_post_mortem self.assertRaises(DummyException, self.server.executeRequest, task) self.assertTrue("raise DummyException" in pm_traceback[-1]) self.server.application = orig_app pdb.post_mortem = orig_post_mortem def test_suite(): return unittest.TestSuite(( unittest.makeSuite(Tests), unittest.makeSuite(PMDBTests), )) if __name__ == '__main__': unittest.main(defaultTest='test_suite') zope.server-3.8.6/src/zope/server/http/tests/test_commonaccesslogger.py0000664000175000017500000000336011701734336025531 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Common Access Logger tests """ import unittest import logging class TestCommonAccessLogger(unittest.TestCase): def test_default_constructor(self): from zope.server.http.commonaccesslogger import CommonAccessLogger from zope.server.logger.unresolvinglogger import UnresolvingLogger from zope.server.logger.pythonlogger import PythonLogger logger = CommonAccessLogger() # CommonHitLogger is registered as an argumentless factory via # ZCML, so the defaults should be sensible self.assert_(isinstance(logger.output, UnresolvingLogger)) self.assert_(isinstance(logger.output.logger, PythonLogger)) self.assert_(logger.output.logger.name, 'accesslog') self.assert_(logger.output.logger.level, logging.INFO) # TODO: please add unit tests for other methods as well: # compute_timezone_for_log # log_date_string # log def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestCommonAccessLogger)) return suite if __name__ == '__main__': unittest.main(defaultTest="test_suite") zope.server-3.8.6/src/zope/server/http/tests/test_httpserver.py0000664000175000017500000003610611701734336024071 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Test HTTP Server """ import unittest from asyncore import socket_map, poll import socket from threading import Thread, Event from zope.server.taskthreads import ThreadedTaskDispatcher from zope.server.adjustments import Adjustments from zope.server.interfaces import ITask from zope.server.tests.asyncerror import AsyncoreErrorHook from zope.interface import implements from httplib import HTTPConnection from httplib import HTTPResponse as ClientHTTPResponse from time import sleep, time td = ThreadedTaskDispatcher() LOCALHOST = '127.0.0.1' SERVER_PORT = 0 # Set these port numbers to 0 to auto-bind, or CONNECT_TO_PORT = 0 # use specific numbers to inspect using TCPWatch. my_adj = Adjustments() # Reduce overflows to make testing easier. my_adj.outbuf_overflow = 10000 my_adj.inbuf_overflow = 10000 class SleepingTask(object): implements(ITask) def service(self): sleep(0.2) def cancel(self): pass def defer(self): pass class Tests(unittest.TestCase, AsyncoreErrorHook): def setUp(self): # import only now to prevent the testrunner from importing it too early # Otherwise dualmodechannel.the_trigger is closed by the ZEO tests from zope.server.http.httpserver import HTTPServer class EchoHTTPServer(HTTPServer): def executeRequest(self, task): headers = task.request_data.headers if 'CONTENT_LENGTH' in headers: cl = headers['CONTENT_LENGTH'] task.response_headers['Content-Length'] = cl instream = task.request_data.getBodyStream() while 1: data = instream.read(8192) if not data: break task.write(data) td.setThreadCount(4) if len(socket_map) != 1: # Let sockets die off. # TODO tests should be more careful to clear the socket map. poll(0.1) self.orig_map_size = len(socket_map) self.hook_asyncore_error() self.server = EchoHTTPServer(LOCALHOST, SERVER_PORT, task_dispatcher=td, adj=my_adj) if CONNECT_TO_PORT == 0: self.port = self.server.socket.getsockname()[1] else: self.port = CONNECT_TO_PORT self.run_loop = 1 self.counter = 0 self.thread_started = Event() self.thread = Thread(target=self.loop) self.thread.setDaemon(True) self.thread.start() self.thread_started.wait(10.0) self.assert_(self.thread_started.isSet()) def tearDown(self): self.run_loop = 0 self.thread.join() td.shutdown() self.server.close() # Make sure all sockets get closed by asyncore normally. timeout = time() + 5 while 1: if len(socket_map) == self.orig_map_size: # Clean! break if time() >= timeout: self.fail('Leaked a socket: %s' % `socket_map`) poll(0.1) self.unhook_asyncore_error() def loop(self): self.thread_started.set() while self.run_loop: self.counter = self.counter + 1 #print 'loop', self.counter poll(0.1) def testEchoResponse(self, h=None, add_headers=None, body=''): if h is None: h = HTTPConnection(LOCALHOST, self.port) headers = {} if add_headers: headers.update(add_headers) headers["Accept"] = "text/plain" # Content-Length header automatically added by HTTPConnection.request #if body: # headers["Content-Length"] = str(int(len(body))) h.request("GET", "/", body, headers) response = h.getresponse() self.failUnlessEqual(int(response.status), 200) length = int(response.getheader('Content-Length', '0')) response_body = response.read() self.failUnlessEqual(length, len(response_body)) self.failUnlessEqual(response_body, body) # HTTP 1.1 requires the server and date header. self.assertEqual(response.getheader('server'), 'zope.server.http') self.assert_(response.getheader('date') is not None) def testMultipleRequestsWithoutBody(self): # Tests the use of multiple requests in a single connection. h = HTTPConnection(LOCALHOST, self.port) for n in range(3): self.testEchoResponse(h) self.testEchoResponse(h, {'Connection': 'close'}) def testMultipleRequestsWithBody(self): # Tests the use of multiple requests in a single connection. h = HTTPConnection(LOCALHOST, self.port) for n in range(3): self.testEchoResponse(h, body='Hello, world!') self.testEchoResponse(h, {'Connection': 'close'}) def testPipelining(self): # Tests the use of several requests issued at once. s = ("GET / HTTP/1.0\r\n" "Connection: %s\r\n" "Content-Length: %d\r\n" "\r\n" "%s") to_send = '' count = 25 for n in range(count): body = "Response #%d\r\n" % (n + 1) if n + 1 < count: conn = 'keep-alive' else: conn = 'close' to_send += s % (conn, len(body), body) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(to_send) for n in range(count): expect_body = "Response #%d\r\n" % (n + 1) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) length = int(response.getheader('Content-Length', '0')) response_body = response.read(length) self.failUnlessEqual(length, len(response_body)) self.failUnlessEqual(response_body, expect_body) def testWithoutCRLF(self): # Tests the use of just newlines rather than CR/LFs. data = "Echo\nthis\r\nplease" s = ("GET / HTTP/1.0\n" "Connection: close\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) length = int(response.getheader('Content-Length', '0')) response_body = response.read(length) self.failUnlessEqual(length, len(data)) self.failUnlessEqual(response_body, data) def testLargeBody(self): # Tests the use of multiple requests in a single connection. h = HTTPConnection(LOCALHOST, self.port) s = 'This string has 32 characters.\r\n' * 32 # 1024 characters. self.testEchoResponse(h, body=(s * 1024)) # 1 MB self.testEchoResponse(h, {'Connection': 'close'}, body=(s * 100)) # 100 KB def testManyClients(self): import sys # Set the number of connections to make. A previous comment said # Linux kernel (2.4.8) doesn't like > 128. # The test used to use 50. Win98SE can't handle that many, dying # with # File "C:\PYTHON23\Lib\httplib.py", line 548, in connect # raise socket.error, msg # error: (10055, 'No buffer space available') nconn = 50 if sys.platform == 'win32': platform = sys.getwindowsversion()[3] if platform < 2: # 0 is Win32s on Windows 3.1 # 1 is 95/98/ME # 2 is NT/2000/XP # Pre-NT. 20 should work. The exact number you can get away # with depends on what you're running at the same time (e.g., # browsers and AIM and email delivery consume sockets too). nconn = 20 conns = [] for n in range(nconn): #print 'open', n, clock() h = HTTPConnection(LOCALHOST, self.port) #h.debuglevel = 1 h.request("GET", "/", headers={"Accept": "text/plain"}) conns.append(h) # If you uncomment the next line, you can raise the # number of connections much higher without running # into delays. #sleep(0.01) responses = [] for h in conns: response = h.getresponse() self.failUnlessEqual(response.status, 200) responses.append(response) for response in responses: response.read() def testThreading(self): # Ensures the correct number of threads keep running. for n in range(4): td.addTask(SleepingTask()) # Try to confuse the task manager. td.setThreadCount(2) td.setThreadCount(1) sleep(0.5) # There should be 1 still running. self.failUnlessEqual(len(td.threads), 1) def testChunkingRequestWithoutContent(self): h = HTTPConnection(LOCALHOST, self.port) h.request("GET", "/", headers={"Accept": "text/plain", "Transfer-Encoding": "chunked"}) h.send("0\r\n\r\n") response = h.getresponse() self.failUnlessEqual(int(response.status), 200) response_body = response.read() self.failUnlessEqual(response_body, '') def testChunkingRequestWithContent(self): control_line="20;\r\n" # 20 hex = 32 dec s = 'This string has 32 characters.\r\n' expect = s * 12 h = HTTPConnection(LOCALHOST, self.port) h.request("GET", "/", headers={"Accept": "text/plain", "Transfer-Encoding": "chunked"}) for n in range(12): h.send(control_line) h.send(s) h.send("0\r\n\r\n") response = h.getresponse() self.failUnlessEqual(int(response.status), 200) response_body = response.read() self.failUnlessEqual(response_body, expect) def testKeepaliveHttp10(self): # Handling of Keep-Alive within HTTP 1.0 data = "Default: Don't keep me alive" s = ("GET / HTTP/1.0\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) connection = response.getheader('Connection', '') # We sent no Connection: Keep-Alive header # Connection: close (or no header) is default. self.failUnless(connection != 'Keep-Alive') # If header Connection: Keep-Alive is explicitly sent, # we want to keept the connection open, we also need to return # the corresponding header data = "Keep me alive" s = ("GET / HTTP/1.0\n" "Connection: Keep-Alive\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) connection = response.getheader('Connection', '') self.failUnlessEqual(connection, 'Keep-Alive') def testKeepaliveHttp11(self): # Handling of Keep-Alive within HTTP 1.1 # All connections are kept alive, unless stated otherwise data = "Default: Keep me alive" s = ("GET / HTTP/1.1\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) self.failUnless(response.getheader('connection') != 'close') # Explicitly set keep-alive data = "Default: Keep me alive" s = ("GET / HTTP/1.1\n" "Connection: keep-alive\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) self.failUnless(response.getheader('connection') != 'close') # no idea why the test publisher handles this request incorrectly # it would be less typing in the test :) # h = HTTPConnection(LOCALHOST, self.port) # h.request("GET", "/") # response = h.getresponse() # self.failUnlessEqual(int(response.status), 200) # self.failUnless(response.getheader('connection') != 'close') # specifying Connection: close explicitly data = "Don't keep me alive" s = ("GET / HTTP/1.1\n" "Connection: close\n" "Content-Length: %d\n" "\n" "%s") % (len(data), data) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((LOCALHOST, self.port)) sock.send(s) response = ClientHTTPResponse(sock) response.begin() self.failUnlessEqual(int(response.status), 200) self.failUnlessEqual(response.getheader('connection'), 'close') class TestHTTPServer(unittest.TestCase): def setUp(self): # Tests.setUp for the explanation why HTTPServer is not imported # at the top from zope.server.http.httpserver import HTTPServer class MyServer(HTTPServer): def __init__(self): # don't call base class, we don't want real sockets here self.server_name = 'example.com' self.port = 8080 self.server = MyServer() def test_getExtraLogMessage(self): self.assertEqual(self.server.getExtraLogMessage(), '\n\tURL: http://example.com:8080/') def test_suite(): loader = unittest.TestLoader() return unittest.TestSuite([ loader.loadTestsFromTestCase(Tests), loader.loadTestsFromTestCase(TestHTTPServer), ]) if __name__=='__main__': unittest.TextTestRunner().run(test_suite()) zope.server-3.8.6/src/zope/server/http/tests/test_httpdate.py0000664000175000017500000000220111701734336023465 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Test HTTP date converters """ import unittest from zope.server.http.http_date import build_http_date, parse_http_date class Tests(unittest.TestCase): # test roundtrip conversion. def testDateRoundTrip(self): from time import time t = int(time()) self.assertEquals(t, parse_http_date(build_http_date(t))) def test_suite(): loader = unittest.TestLoader() return loader.loadTestsFromTestCase(Tests) if __name__=='__main__': unittest.TextTestRunner().run(test_suite()) zope.server-3.8.6/src/zope/server/http/tests/wsgi_app.py0000664000175000017500000000063211701734336022430 0ustar mgmg00000000000000# a WSGI app for testing def test_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return ["Hello world! zope.server is delivering WSGI v%s.%s using %s." % (environ['wsgi.version'] + (environ['wsgi.url_scheme'],))] def test_app_factory(global_config, **local_config): return test_app zope.server-3.8.6/src/zope/server/http/tests/__init__.py0000664000175000017500000000007511701734336022357 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/http/httpserverchannel.py0000664000175000017500000000173611701734336023222 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Server Channel """ from zope.server.serverchannelbase import ServerChannelBase from zope.server.http.httptask import HTTPTask from zope.server.http.httprequestparser import HTTPRequestParser class HTTPServerChannel(ServerChannelBase): """HTTP-specific Server Channel""" task_class = HTTPTask parser_class = HTTPRequestParser zope.server-3.8.6/src/zope/server/http/wsgihttpserver.py0000664000175000017500000001321011701734336022551 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """WSGI-compliant HTTP Server that uses the Zope Publisher for executing a task. """ import asyncore import re import sys from zope.server.http.httpserver import HTTPServer from zope.server.taskthreads import ThreadedTaskDispatcher import zope.security.management def fakeWrite(body): raise NotImplementedError( "Zope 3's HTTP Server does not support the WSGI write() function.") def curriedStartResponse(task): def start_response(status, headers, exc_info=None): if task.wroteResponseHeader() and not exc_info: raise AssertionError("start_response called a second time " "without providing exc_info.") if exc_info: try: if task.wroteResponseHeader(): # higher levels will catch and handle raised exception: # 1. "service" method in httptask.py # 2. "service" method in severchannelbase.py # 3. "handlerThread" method in taskthreads.py raise exc_info[0], exc_info[1], exc_info[2] else: # As per WSGI spec existing headers must be cleared task.accumulated_headers = None task.response_headers = {} finally: exc_info = None # Prepare the headers for output status, reason = re.match('([0-9]*) (.*)', status).groups() task.setResponseStatus(status, reason) task.appendResponseHeaders(['%s: %s' % i for i in headers]) # Return the write method used to write the response data. return fakeWrite return start_response class WSGIHTTPServer(HTTPServer): """Zope Publisher-specific WSGI-compliant HTTP Server""" application = None def __init__(self, application, sub_protocol=None, *args, **kw): if sys.platform[:3] == "win" and args[0] == 'localhost': args = ('',) + args[1:] self.application = application if sub_protocol: self.SERVER_IDENT += ' (%s)' %str(sub_protocol) HTTPServer.__init__(self, *args, **kw) def _constructWSGIEnvironment(self, task): env = task.getCGIEnvironment() # deduce the URL scheme (http or https) if (env.get('HTTPS', '').lower() == "on" or env.get('SERVER_PORT_SECURE') == "1"): protocol = 'https' else: protocol = 'http' # the following environment variables are required by the WSGI spec env['wsgi.version'] = (1,0) env['wsgi.url_scheme'] = protocol env['wsgi.errors'] = sys.stderr # apps should use the logging module env['wsgi.multithread'] = True env['wsgi.multiprocess'] = True env['wsgi.run_once'] = False env['wsgi.input'] = task.request_data.getBodyStream() # Add some proprietary proxy information. # Note: Derived request parsers might not have these new attributes, # so fail gracefully. try: env['zserver.proxy.scheme'] = task.request_data.proxy_scheme env['zserver.proxy.host'] = task.request_data.proxy_netloc except AttributeError: pass return env def executeRequest(self, task): """Overrides HTTPServer.executeRequest().""" env = self._constructWSGIEnvironment(task) # Call the application to handle the request and write a response result = self.application(env, curriedStartResponse(task)) # By iterating manually at this point, we execute task.write() # multiple times, allowing partial data to be sent. for value in result: task.write(value) class PMDBWSGIHTTPServer(WSGIHTTPServer): """Enter the post-mortem debugger when there's an error""" def executeRequest(self, task): """Overrides HTTPServer.executeRequest().""" env = self._constructWSGIEnvironment(task) env['wsgi.handleErrors'] = False # Call the application to handle the request and write a response try: result = self.application(env, curriedStartResponse(task)) # By iterating manually at this point, we execute task.write() # multiple times, allowing partial data to be sent. for value in result: task.write(value) except: import sys, pdb print "%s:" % sys.exc_info()[0] print sys.exc_info()[1] zope.security.management.restoreInteraction() try: pdb.post_mortem(sys.exc_info()[2]) raise finally: zope.security.management.endInteraction() def run_paste(wsgi_app, global_conf, name='zope.server.http', host='127.0.0.1', port=8080, threads=4): port = int(port) threads = int(threads) task_dispatcher = ThreadedTaskDispatcher() task_dispatcher.setThreadCount(threads) server = WSGIHTTPServer(wsgi_app, name, host, port, task_dispatcher=task_dispatcher) asyncore.loop() zope.server-3.8.6/src/zope/server/http/httprequestparser.py0000664000175000017500000001523611701734336023270 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """HTTP Request Parser This server uses asyncore to accept connections and do initial processing but threads to do work. """ import re from urllib import unquote import urlparse from zope.server.fixedstreamreceiver import FixedStreamReceiver from zope.server.buffers import OverflowableBuffer from zope.server.utilities import find_double_newline from zope.server.interfaces import IStreamConsumer from zope.interface import implements try: from cStringIO import StringIO except ImportError: from StringIO import StringIO class HTTPRequestParser(object): """A structure that collects the HTTP request. Once the stream is completed, the instance is passed to a server task constructor. """ implements(IStreamConsumer) completed = 0 # Set once request is completed. empty = 0 # Set if no request was made. header_plus = '' chunked = 0 content_length = 0 body_rcv = None # Other attributes: first_line, header, headers, command, uri, version, # path, query, fragment # headers is a mapping containing keys translated to uppercase # with dashes turned into underscores. def __init__(self, adj): """ adj is an Adjustments object. """ self.headers = {} self.adj = adj def received(self, data): """ Receives the HTTP stream for one request. Returns the number of bytes consumed. Sets the completed flag once both the header and the body have been received. """ if self.completed: return 0 # Can't consume any more. datalen = len(data) br = self.body_rcv if br is None: # In header. s = self.header_plus + data index = find_double_newline(s) if index >= 0: # Header finished. header_plus = s[:index] consumed = len(data) - (len(s) - index) self.in_header = 0 # Remove preceeding blank lines. header_plus = header_plus.lstrip() if not header_plus: self.empty = 1 self.completed = 1 else: self.parse_header(header_plus) if self.body_rcv is None: self.completed = 1 return consumed else: # Header not finished yet. self.header_plus = s return datalen else: # In body. consumed = br.received(data) if br.completed: self.completed = 1 return consumed def parse_header(self, header_plus): """ Parses the header_plus block of text (the headers plus the first line of the request). """ index = header_plus.find('\n') if index >= 0: first_line = header_plus[:index].rstrip() header = header_plus[index + 1:] else: first_line = header_plus.rstrip() header = '' self.first_line = first_line self.header = header lines = self.get_header_lines() headers = self.headers for line in lines: index = line.find(':') if index > 0: key = line[:index] value = line[index + 1:].strip() key1 = key.upper().replace('-', '_') # If a header already exists, we append subsequent values # seperated by a comma. Applications already need to handle # the comma seperated values, as HTTP front ends might do # the concatenation for you (behavior specified in RFC2616). try: headers[key1] += ', %s' % value except KeyError: headers[key1] = value # else there's garbage in the headers? command, uri, version = self.crack_first_line() self.command = str(command) self.uri = str(uri) self.version = version self.split_uri() if version == '1.1': te = headers.get('TRANSFER_ENCODING', '') if te == 'chunked': from zope.server.http.chunking import ChunkedReceiver self.chunked = 1 buf = OverflowableBuffer(self.adj.inbuf_overflow) self.body_rcv = ChunkedReceiver(buf) if not self.chunked: try: cl = int(headers.get('CONTENT_LENGTH', 0)) except ValueError: cl = 0 self.content_length = cl if cl > 0: buf = OverflowableBuffer(self.adj.inbuf_overflow) self.body_rcv = FixedStreamReceiver(cl, buf) def get_header_lines(self): """ Splits the header into lines, putting multi-line headers together. """ r = [] lines = self.header.split('\n') for line in lines: if line and line[0] in ' \t': r[-1] = r[-1] + line[1:] else: r.append(line) return r first_line_re = re.compile ( '([^ ]+) ((?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)(( HTTP/([0-9.]+))$|$)') def crack_first_line(self): r = self.first_line m = self.first_line_re.match (r) if m is not None and m.end() == len(r): if m.group(3): version = m.group(5) else: version = None return m.group(1).upper(), m.group(2), version else: return None, None, None def split_uri(self): (self.proxy_scheme, self.proxy_netloc, path, self.query, self.fragment) = \ urlparse.urlsplit(self.uri) if path and '%' in path: path = unquote(path) self.path = path if self.query == '': self.query = None def getBodyStream(self): body_rcv = self.body_rcv if body_rcv is not None: return body_rcv.getfile() else: return StringIO('') zope.server-3.8.6/src/zope/server/http/__init__.py0000664000175000017500000000007511701734336021215 0ustar mgmg00000000000000# # This file is necessary to make this directory a package. zope.server-3.8.6/src/zope/server/adjustments.py0000664000175000017500000000526411701734341021041 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Adjustments are tunable parameters. """ import socket from zope.server import maxsockets class Adjustments(object): """This class contains tunable communication parameters. You can either change default_adj to adjust parameters for all sockets, or you can create a new instance of this class, change its attributes, and pass it to the channel constructors. """ # backlog is the argument to pass to socket.listen(). backlog = 1024 # recv_bytes is the argument to pass to socket.recv(). recv_bytes = 8192 # send_bytes is the number of bytes to send to socket.send(). # Multiples of 9000 should avoid partly-filled packets, but don't # set this larger than the TCP write buffer size. In Linux, # /proc/sys/net/ipv4/tcp_wmem controls the minimum, default, and # maximum sizes of TCP write buffers. send_bytes = 9000 # copy_bytes is the number of bytes to copy from one file to another. copy_bytes = 65536 # Create a tempfile if the pending output data gets larger # than outbuf_overflow. With RAM so cheap, this probably # ought to be set to the 16-32 MB range (circa 2001) for # good performance with big transfers. The default is # conservative. outbuf_overflow = 1050000 # Create a tempfile if the data received gets larger # than inbuf_overflow. inbuf_overflow = 525000 # Stop accepting new connections if too many are already active. connection_limit = maxsockets.max_select_sockets() - 3 # Safe # Minimum seconds between cleaning up inactive channels. cleanup_interval = 300 # Maximum seconds to leave an inactive connection open. channel_timeout = 900 # Boolean: turn off to not log premature client disconnects. log_socket_errors = 1 # The socket options to set on receiving a connection. # It is a list of (level, optname, value) tuples. # TCP_NODELAY is probably good for Zope, since Zope buffers # data itself. socket_options = [ (socket.SOL_TCP, socket.TCP_NODELAY, 1), ] default_adj = Adjustments() zope.server-3.8.6/src/zope/server/interfaces/0000775000175000017500000000000011701734350020242 5ustar mgmg00000000000000zope.server-3.8.6/src/zope/server/interfaces/logger.py0000664000175000017500000000223011701734337022075 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Logger interfaces """ from zope.interface import Interface class IRequestLogger(Interface): """This interface describes a requets logger, which logs ip addresses and messages. """ def logRequest(ip, message): """Logs the ip address and message at the appropriate place.""" class IMessageLogger(Interface): """This interface describes a message logger, which logs with the resolution of one message. """ def logMessage(message): """Logs the message at the appropriate place.""" zope.server-3.8.6/src/zope/server/interfaces/ftp.py0000664000175000017500000002714411701734337021422 0ustar mgmg00000000000000############################################################################## # Copyright (c) 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. ############################################################################## """FTP server specific interfaces. """ from zope.interface import Interface class IFTPCommandHandler(Interface): """This interface defines all the FTP commands that are supported by the server. Every command takes the command line as first arguments, since it is responsible """ def cmd_abor(args): """Abort operation. No read access required. """ def cmd_appe(args): """Append to a file. Write access required. """ def cmd_cdup(args): """Change to parent of current working directory. """ def cmd_cwd(args): """Change working directory. """ def cmd_dele(args): """Delete a file. Write access required. """ def cmd_help(args): """Give help information. No read access required. """ def cmd_list(args): """Give list files in a directory or displays the info of one file. """ def cmd_mdtm(args): """Show last modification time of file. Example output: 213 19960301204320 Geez, there seems to be a second syntax for this fiel, where one can also set the modification time using: MDTM datestring pathname """ def cmd_mkd(args): """Make a directory. Write access required. """ def cmd_mode(args): """Set file transfer mode. No read access required. Obselete. """ def cmd_nlst(args): """Give name list of files in directory. """ def cmd_noop(args): """Do nothing. No read access required. """ def cmd_pass(args): """Specify password. """ def cmd_pasv(args): """Prepare for server-to-server transfer. No read access required. """ def cmd_port(args): """Specify data connection port. No read access required. """ def cmd_pwd(args): """Print the current working directory. """ def cmd_quit(args): """Terminate session. No read access required. """ def cmd_rest(args): """Restart incomplete transfer. """ def cmd_retr(args): """Retrieve a file. """ def cmd_rmd(args): """Remove a directory. Write access required. """ def cmd_rnfr(args): """Specify rename-from file name. Write access required. """ def cmd_rnto(args): """Specify rename-to file name. Write access required. """ def cmd_size(args): """Return size of file. """ def cmd_stat(args): """Return status of server. No read access required. """ def cmd_stor(args): """Store a file. Write access required. """ def cmd_stru(args): """Set file transfer structure. Obselete.""" def cmd_syst(args): """Show operating system type of server system. No read access required. Replying to this command is of questionable utility, because this server does not behave in a predictable way w.r.t. the output of the LIST command. We emulate Unix ls output, but on win32 the pathname can contain drive information at the front Currently, the combination of ensuring that os.sep == '/' and removing the leading slash when necessary seems to work. [cd'ing to another drive also works] This is how wuftpd responds, and is probably the most expected. The main purpose of this reply is so that the client knows to expect Unix ls-style LIST output. one disadvantage to this is that some client programs assume they can pass args to /bin/ls. a few typical responses: 215 UNIX Type: L8 (wuftpd) 215 Windows_NT version 3.51 215 VMS MultiNet V3.3 500 'SYST': command not understood. (SVR4) """ def cmd_type(args): """Specify data transfer type. No read access required. """ def cmd_user(args): """Specify user name. No read access required. """ # this is the command list from the wuftpd man page # '!' requires write access # not_implemented_commands = { 'acct': 'specify account (ignored)', 'allo': 'allocate storage (vacuously)', 'site': 'non-standard commands (see next section)', 'stou': 'store a file with a unique name', #! 'xcup': 'change to parent of current working directory (deprecated)', 'xcwd': 'change working directory (deprecated)', 'xmkd': 'make a directory (deprecated)', #! 'xpwd': 'print the current working directory (deprecated)', 'xrmd': 'remove a directory (deprecated)', #! } class IFileSystemAccess(Interface): """Provides authenticated access to a filesystem.""" def authenticate(credentials): """Verifies filesystem access based on the presented credentials. Should raise zope.security.interfaces.Unauthorized if the user can not be authenticated. This method checks only general access and is not used for each call to open(). Rather, open() should do its own verification. Credentials are passed as (username, password) tuples. """ def open(credentials): """Returns an IFileSystem. Should raise zope.security.interfaces.Unauthorized if the user can not be authenticated. Credentials are passed as (username, password) tuples. """ class IFileSystem(Interface): """An abstract filesystem. Opening files for reading, and listing directories, should return a producer. All paths are POSIX paths, even when run on Windows, which mainly means that FS implementations always expect forward slashes, and filenames are case-sensitive. `IFileSystem`, in generel, could be created many times per request. Thus it is not advisable to store state in them. However, if you have a special kind of `IFileSystemAccess` object that somhow manages an `IFileSystem` for each set of credentials, then it would be possible to store some state on this obejct. """ def type(path): """Return the file type at `path`. The return valie is 'd', for a directory, 'f', for a file, and None if there is no file at `path`. This method doesn't raise exceptions. """ def names(path, filter=None): """Return a sequence of the names in a directory. If `filter` is not None, include only those names for which `filter` returns a true value. """ def ls(path, filter=None): """Return a sequence of information objects. Returm item info objects (see the ls_info operation) for the files in a directory. If `filter` is not None, include only those names for which `filter` returns a true value. """ def readfile(path, outstream, start=0, end=None): """Outputs the file at `path` to a stream. Data are copied starting from `start`. If `end` is not None, data are copied up to `end`. """ def lsinfo(path): """Return information for a unix-style ls listing for `path`. Information is returned as a dictionary containing the following keys: type The path type, either 'd' or 'f'. owner_name Defaults to "na". Must not include spaces. owner_readable Defaults to True. owner_writable Defaults to True. owner_executable Defaults to True for directories and False otherwise. group_name Defaults to "na". Must not include spaces. group_readable Defaults to True. group_writable Defaults to True. group_executable Defaults to True for directories and False otherwise. other_readable Defaults to False. other_writable Defaults to False. other_executable Defaults to True for directories and false otherwise. mtime Optional time, as a datetime.datetime object. nlinks The number of links. Defaults to 1. size The file size. Defaults to 0. name The file name. """ def mtime(path): """Return the modification time for the file at `path`. This method returns the modification time. It is assumed that the path exists. You can use the `type(path)` method to determine whether `path` points to a valid file. If the modification time is unknown, then return `None`. """ def size(path): """Return the size of the file at path. This method returns the modification time. It is assumed that the path exists. You can use the `type(path)` method to determine whether `path` points to a valid file. """ def mkdir(path): """Create a directory. If it is not possible or allowed to create the directory, an `OSError` should be raised describing the reason of failure. """ def remove(path): """Remove a file. Same as unlink. If it is not possible or allowed to remove the file, an `OSError` should be raised describing the reason of failure. """ def rmdir(path): """Remove a directory. If it is not possible or allowed to remove the directory, an `OSError` should be raised describing the reason of failure. """ def rename(old, new): """Rename a file or directory.""" def writefile(path, instream, start=None, end=None, append=False): """Write data to a file. Both `start` and `end` must be either None or a non-negative integer. If `append` is true, `start` and `end` are ignored. If `start` or `end` is not None, they specify the part of the file that is to be written. If `end` is None, the file is truncated after the data are written. If `end` is not None, any parts of the file after `end` are left unchanged. Note that if `end` is not `None`, and there is not enough data in the `instream` it will fill the file up to `end`, then the missing data are undefined. If both `start` is `None` and `end` is `None`, then the file contents are overwritten. If `start` is specified and the file doesn't exist or is shorter than `start`, the data in the file before `start` file will be undefined. If you do not want to handle incorrect starting and ending indices, you can also raise an `IOError`, which will be properly handled by the server. """ def writable(path): """Return boolean indicating whether a file at path is writable. Note that a true value should be returned if the file doesn't exist but its directory is writable. """ zope.server-3.8.6/src/zope/server/interfaces/__init__.py0000664000175000017500000002326511701734337022370 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Server interfaces. """ from zope.interface import Interface from zope.interface import Attribute class ISocket(Interface): """Represents a socket. Note: Most of this documentation is taken from the Python Library Reference. """ def listen(backlog): """Listen for connections made to the socket. The 'backlog' argument specifies the maximum number of queued connections and should be at least 1; the maximum value is system-dependent (usually 5). """ def bind(addr): """Bind the socket to address. The socket must not already be bound. """ def connect(address): """Connect to a remote socket at address.""" def accept(): """Accept a connection. The socket must be bound to an address and listening for connections. The return value is a pair (conn, address) where conn is a new socket object usable to send and receive data on the connection, and address is the address bound to the socket on the other end of the connection. """ def recv(buffer_size): """Receive data from the socket. The return value is a string representing the data received. The maximum amount of data to be received at once is specified by bufsize. See the Unix manual page recv(2) for the meaning of the optional argument flags; it defaults to zero. """ def send(data): """Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. """ def close(): """Close the socket. All future operations on the socket object will fail. The remote end will receive no more data (after queued data is flushed). Sockets are automatically closed when they are garbage-collected. """ class ITaskDispatcher(Interface): """An object that accepts tasks and dispatches them to threads. """ def setThreadCount(count): """Sets the number of handler threads. """ def addTask(task): """Receives a task and dispatches it to a thread. Note that, depending on load, a task may have to wait a while for its turn. """ def shutdown(cancel_pending=True, timeout=5): """Shuts down all handler threads and may cancel pending tasks. """ def getPendingTasksEstimate(): """Returns an estimate of the number of tasks waiting to be serviced. This method may be useful for monitoring purposes. If the number of pending tasks is continually climbing, your server is becoming overloaded and the operator should be notified. """ class ITask(Interface): """ The interface expected of an object placed in the queue of a ThreadedTaskDispatcher. Provides facilities for executing or canceling the task. """ def service(): """ Services the task. Either service() or cancel() is called for every task queued. """ def cancel(): """ Called instead of service() during shutdown or if an exception occurs that prevents the task from being serviced. Must return quickly and should not throw exceptions. """ def defer(): """ Called just before the task is queued to be executed in a different thread. """ class IDispatcherEventHandler(Interface): """The Dispatcher can receive several different types of events. This interface describes the necessary methods that handle these common event types. """ def handle_read_event(): """Given a read event, a server has to handle the event and read the input from the client. """ def handle_write_event(): """Given a write event, a server has to handle the event and write the output to the client. """ def handle_expt_event(): """An exception event was handed to the server. """ def handle_error(): """An error occurred, but we are still trying to fix it. """ def handle_expt(): """Handle unhandled exceptions. This is usually a time to log. """ def handle_read(): """Read output from client. """ def handle_write(): """Write output via the socket to the client. """ def handle_connect(): """A client requests a connection, now we need to do soemthing. """ def handle_accept(): """A connection is accepted. """ def handle_close(): """A connection is being closed. """ class IStreamConsumer(Interface): """Consumes a data stream until reaching a completion point. The actual amount to be consumed might not be known ahead of time. """ def received(data): """Accepts data, returning the number of bytes consumed.""" completed = Attribute( 'completed', 'Set to a true value when finished consuming data.') class IServer(Interface): """This interface describes the basic base server. The most unusual part about the Zope servers (since they all implement this interface or inherit its base class) is that it uses a mix of asynchronous and thread-based mechanism to serve. While the low-level socket listener uses async, the actual request is executed in a thread. This is important because even if a request takes a long time to process, the server can service other requests simultaneously. """ channel_class = Attribute(""" The channel class defines the type of channel to be used by the server. See IServerChannel for more information. """) SERVER_IDENT = Attribute(""" This string identifies the server. By default this is 'zope.server.' and should be overridden. """) class IDispatcherLogging(Interface): """This interface provides methods through which the Dispatcher will write its logs. A distinction is made between hit and message logging, since they often go to different output types and can have very different structure. """ def log (message): """Logs general requests made to the server. """ def log_info(message, type='info'): """Logs informational messages, warnings and errors. """ class IServerChannel(Interface): parser_class = Attribute("""Subclasses must provide a parser class""") task_class = Attribute("""Specifies the ITask class to be used for generating tasks.""") def queue_task(task): """Queues a channel-related task to be processed in sequence. """ class IDispatcher(ISocket, IDispatcherEventHandler, IDispatcherLogging): """The dispatcher is the most low-level component of a server. 1. It manages the socket connections and distributes the request to the appropriate channel. 2. It handles the events passed to it, such as reading input, writing output and handling errors. More about this functionality can be found in IDispatcherEventHandler. 3. It handles logging of the requests passed to the server as well as other informational messages and erros. Please see IDispatcherLogging for more details. Note: Most of this documentation is taken from the Python Library Reference. """ def add_channel(map=None): """After the low-level socket connection negotiation is completed, a channel is created that handles all requests and responses until the end of the connection. """ def del_channel(map=None): """Delete a channel. This should include also closing the socket to the client. """ def create_socket(family, type): """This is identical to the creation of a normal socket, and will use the same options for creation. Refer to the socket documentation for information on creating sockets. """ def readable(): """Each time through the select() loop, the set of sockets is scanned, and this method is called to see if there is any interest in reading. The default method simply returns 1, indicating that by default, all channels will be interested. """ def writable(): """Each time through the select() loop, the set of sockets is scanned, and this method is called to see if there is any interest in writing. The default method simply returns 1, indicating that by default, all channels will be interested. """ zope.server-3.8.6/src/zope/server/serverbase.py0000664000175000017500000001250511701734341020635 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Server Base Class This module provides a base implementation for a channel-based server. It can only be used as a mix-in to actual server implementations. """ import asyncore import logging import socket from zope.server.adjustments import default_adj from zope.server.interfaces import IServer from zope.interface import implements class ServerBase(asyncore.dispatcher, object): """Async. server base for launching derivatives of ServerChannelBase.""" implements(IServer) # See zope.server.interfaces.IServer channel_class = None # Override with a channel class. SERVER_IDENT = 'zope.server.serverbase' # Override. def __init__(self, ip, port, task_dispatcher=None, adj=None, start=1, hit_log=None, verbose=0): if adj is None: adj = default_adj self.adj = adj asyncore.dispatcher.__init__(self) self.port = port self.task_dispatcher = task_dispatcher self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((ip, port)) self.verbose = verbose self.hit_log = hit_log self.logger = logging.getLogger(self.__class__.__name__) self.server_name = self.computeServerName(ip) if start: self.accept_connections() def log(self, message): """See zope.server.interfaces.IDispatcherLogging""" # Override asyncore's default log() self.logger.info(message) level_mapping = { 'info': logging.INFO, 'error': logging.ERROR, 'warning': logging.WARN, } def log_info(self, message, type='info'): """See zope.server.interfaces.IDispatcherLogging""" self.logger.log(self.level_mapping.get(type, logging.INFO), message) def computeServerName(self, ip=''): """Given an IP, try to determine the server name.""" if ip: server_name = str(ip) else: server_name = str(socket.gethostname()) # Convert to a host name if necessary. is_hostname = 0 for c in server_name: if c != '.' and not c.isdigit(): is_hostname = 1 break if not is_hostname: if self.verbose: self.log_info('Computing hostname', 'info') try: server_name = socket.gethostbyaddr(server_name)[0] except socket.error: if self.verbose: self.log_info('Cannot do reverse lookup', 'info') return server_name def accept_connections(self): self.accepting = 1 self.socket.listen(self.adj.backlog) # Circumvent asyncore's NT limit if self.verbose: self.log_info('%s started.\n' '\tHostname: %s\n\tPort: %d%s' % ( self.SERVER_IDENT, self.server_name, self.port, self.getExtraLogMessage() )) def getExtraLogMessage(self): r"""Additional information to be logged on startup. If not empty, should start with '\n\t', and every line break should be followed by a '\t'. """ return '' def addTask(self, task): """See zope.server.interfaces.ITaskDispatcher""" td = self.task_dispatcher if td is not None: td.addTask(task) else: task.service() def readable(self): """See zope.server.interfaces.IDispatcher""" return (self.accepting and len(asyncore.socket_map) < self.adj.connection_limit) def writable(self): """See zope.server.interfaces.IDispatcher""" return 0 def handle_read(self): """See zope.server.interfaces.IDispatcherEventHandler""" pass def handle_connect(self): """See zope.server.interfaces.IDispatcherEventHandler""" pass def handle_accept(self): """See zope.server.interfaces.IDispatcherEventHandler""" try: v = self.accept() if v is None: return conn, addr = v except socket.error: # Linux: On rare occasions we get a bogus socket back from # accept. socketmodule.c:makesockaddr complains that the # address family is unknown. We don't want the whole server # to shut down because of this. if self.adj.log_socket_errors: self.log_info ('warning: server accept() threw an exception', 'warning') return for (level, optname, value) in self.adj.socket_options: conn.setsockopt(level, optname, value) self.channel_class(self, conn, addr, self.adj) zope.server-3.8.6/src/zope/server/__init__.py0000664000175000017500000000200611701734341020226 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Zope 3's Servers This package contains generic base classes for channel-based servers, the servers themselves and helper objects, such as tasks and requests. """ import asyncore from zope.server.interfaces import IDispatcher from zope.interface import classImplements # Tell the the async.dispatcher that it implements IDispatcher. classImplements(asyncore.dispatcher, IDispatcher) zope.server-3.8.6/src/zope/server/fixedstreamreceiver.py0000664000175000017500000000272111701734341022533 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Fixed Stream Receiver """ from zope.server.interfaces import IStreamConsumer from zope.interface import implements class FixedStreamReceiver(object): implements(IStreamConsumer) # See IStreamConsumer completed = 0 def __init__(self, cl, buf): self.remain = cl self.buf = buf def received(self, data): 'See IStreamConsumer' rm = self.remain if rm < 1: self.completed = 1 # Avoid any chance of spinning return 0 datalen = len(data) if rm <= datalen: self.buf.append(data[:rm]) self.remain = 0 self.completed = 1 return rm else: self.buf.append(data) self.remain -= datalen return datalen def getfile(self): return self.buf.getfile() zope.server-3.8.6/src/zope/server/dualmodechannel.py0000664000175000017500000001350411701734341021617 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2001, 2002 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Dual-mode channel """ import asyncore import socket from time import time from zope.server import trigger from zope.server.adjustments import default_adj from zope.server.buffers import OverflowableBuffer # Create the main trigger if it doesn't exist yet. the_trigger = trigger.trigger() class DualModeChannel(asyncore.dispatcher): """Channel that switches between asynchronous and synchronous mode. Call set_sync() before using a channel in a thread other than the thread handling the main loop. Call set_async() to give the channel back to the thread handling the main loop. """ # will_close is set to True to close the socket. will_close = False # boolean: async or sync mode async_mode = True def __init__(self, conn, addr, adj=None): self.addr = addr if adj is None: adj = default_adj self.adj = adj self.outbuf = OverflowableBuffer(adj.outbuf_overflow) self.creation_time = time() asyncore.dispatcher.__init__(self, conn) # # ASYNCHRONOUS METHODS # def handle_close(self): self.close() def writable(self): if not self.async_mode: return 0 return self.will_close or self.outbuf def handle_write(self): if not self.async_mode: return if self.outbuf: try: self._flush_some() except socket.error: self.handle_comm_error() elif self.will_close: self.close() self.last_activity = time() def readable(self): if not self.async_mode: return 0 return not self.will_close def handle_read(self): if not self.async_mode or self.will_close: return try: data = self.recv(self.adj.recv_bytes) except socket.error: self.handle_comm_error() return self.last_activity = time() self.received(data) def received(self, data): """ Override to receive data in async mode. """ pass def handle_comm_error(self): """ Designed for handling communication errors that occur during asynchronous operations *only*. Probably should log this, but in a different place. """ self.handle_error() def set_sync(self): """Switches to synchronous mode. The main thread will stop calling received(). """ self.async_mode = False # # SYNCHRONOUS METHODS # def flush(self, block=True): """Sends pending data. If block is set, this pauses the application. If it is turned off, only the amount of data that can be sent without blocking is sent. """ if not block: while self._flush_some(): pass return blocked = False try: while self.outbuf: # We propagate errors to the application on purpose. if not blocked: self.socket.setblocking(1) blocked = True self._flush_some() finally: if blocked: self.socket.setblocking(0) def set_async(self): """Switches to asynchronous mode. The main thread will begin calling received() again. """ self.async_mode = True self.pull_trigger() self.last_activity = time() # # METHODS USED IN BOTH MODES # def write(self, data): wrote = 0 if isinstance(data, str): if data: self.outbuf.append(data) wrote = len(data) else: for v in data: if v: self.outbuf.append(v) wrote += len(v) while len(self.outbuf) >= self.adj.send_bytes: # Send what we can without blocking. # We propagate errors to the application on purpose # (to stop the application if the connection closes). if not self._flush_some(): break return wrote def pull_trigger(self): """Wakes up the main loop. """ the_trigger.pull_trigger() def _flush_some(self): """Flushes data. Returns 1 if some data was sent.""" outbuf = self.outbuf if outbuf and self.connected: chunk = outbuf.get(self.adj.send_bytes) num_sent = self.send(chunk) if num_sent: outbuf.skip(num_sent, 1) return 1 return 0 def close_when_done(self): # Flush all possible. while self._flush_some(): pass self.will_close = True if not self.async_mode: # For safety, don't close the socket until the # main thread calls handle_write(). self.async_mode = True self.pull_trigger() def close(self): # Always close in asynchronous mode. If the connection is # closed in a thread, the main loop can end up with a bad file # descriptor. assert self.async_mode self.connected = False asyncore.dispatcher.close(self) zope.server-3.8.6/src/zope/__init__.py0000664000175000017500000000007011701734341016717 0ustar mgmg00000000000000__import__('pkg_resources').declare_namespace(__name__) zope.server-3.8.6/bootstrap.py0000664000175000017500000000330211701734341015432 0ustar mgmg00000000000000############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Bootstrap a buildout-based project Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. """ import os, shutil, sys, tempfile, urllib2 tmpeggs = tempfile.mkdtemp() ez = {} exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' ).read() in ez ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) import pkg_resources cmd = 'from setuptools.command.easy_install import main; main()' if sys.platform == 'win32': cmd = '"%s"' % cmd # work around spawn lamosity on windows ws = pkg_resources.working_set assert os.spawnle( os.P_WAIT, sys.executable, sys.executable, '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout', dict(os.environ, PYTHONPATH= ws.find(pkg_resources.Requirement.parse('setuptools')).location ), ) == 0 ws.add_entry(tmpeggs) ws.require('zc.buildout') import zc.buildout.buildout zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) shutil.rmtree(tmpeggs) zope.server-3.8.6/README.txt0000664000175000017500000000114411701734341014543 0ustar mgmg00000000000000This package contains generic base classes for channel-based servers, the servers themselves and helper objects, such as tasks and requests. ============ WSGI Support ============ `zope.server`'s HTTP server comes with WSGI_ support. ``zope.server.http.wsgihttpserver.WSGIHTTPServer`` can act as a WSGI gateway. There's also an entry point for PasteDeploy_ that lets you use zope.server's WSGI gateway from a configuration file, e.g.:: [server:main] use = egg:zope.server host = 127.0.0.1 port = 8080 .. _WSGI: http://www.python.org/dev/peps/pep-0333/ .. _PasteDeploy: http://pythonpaste.org/deploy/ zope.server-3.8.6/buildout.cfg0000664000175000017500000000031111701734341015350 0ustar mgmg00000000000000[buildout] develop = . parts = test paste-test [paste-test] recipe = zc.recipe.egg eggs = zope.server PasteDeploy PasteScript [test] recipe = zc.recipe.testrunner eggs = zope.server [test] zope.server-3.8.6/LICENSE.txt0000664000175000017500000000402611701734341014672 0ustar mgmg00000000000000Zope Public License (ZPL) Version 2.1 A copyright notice accompanies this license document that identifies the copyright holders. This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). 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 accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``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 THE COPYRIGHT HOLDERS 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. zope.server-3.8.6/PKG-INFO0000664000175000017500000001465511701734350014155 0ustar mgmg00000000000000Metadata-Version: 1.0 Name: zope.server Version: 3.8.6 Summary: Zope Server (Web and FTP) Home-page: http://pypi.python.org/pypi/zope.server Author: Zope Foundation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: This package contains generic base classes for channel-based servers, the servers themselves and helper objects, such as tasks and requests. ============ WSGI Support ============ `zope.server`'s HTTP server comes with WSGI_ support. ``zope.server.http.wsgihttpserver.WSGIHTTPServer`` can act as a WSGI gateway. There's also an entry point for PasteDeploy_ that lets you use zope.server's WSGI gateway from a configuration file, e.g.:: [server:main] use = egg:zope.server host = 127.0.0.1 port = 8080 .. _WSGI: http://www.python.org/dev/peps/pep-0333/ .. _PasteDeploy: http://pythonpaste.org/deploy/ ======= CHANGES ======= 3.8.6 (2012-01-07) ------------------ - On startup, HTTPServer prints a clickable URL after the hostname/port. 3.8.5 (2011-09-13) ------------------ - fixed bug: requests lasting over 15 minutes were sometimes closed prematurely. 3.8.4 (2011-06-07) ------------------ - Fix syntax error in tests on Python < 2.6. 3.8.3 (2011-05-18) ------------------ - Made ``start_response`` method of WSGI server implementation more compliant with spec: http://www.python.org/dev/peps/pep-0333/#the-start-response-callable 3.8.2 (2010-12-04) ------------------ - Corrected license version in ``zope/server/http/tests/test_wsgiserver.py``. 3.8.1 (2010-08-24) ------------------ - When the result of a WSGI application was received, ``task.write()`` was only called once to transmit the data. This prohibited the transmission of partial results. Now the WSGI server iterates through the result itself making multiple ``task.write()`` calls, which will cause partial data to be transmitted. - Created a second test case instance for the post-mortem WSGI server, so it is tested as well. - Using python's ``doctest`` module instead of deprecated ``zope.testing.doctest``. 3.8.0 (2010-08-05) ------------------ - Implemented correct server proxy behavior. The HTTP server would always add a "Server" and "Date" response header to the list of response headers regardless whether one had been set already. The HTTP 1.1 spec specifies that a proxy server must not modify the "Server" and "Date" header but add a "Via" header instead. 3.7.0 (2010-08-01) ------------------ - Implemented proxy support. Proxy requests contain a full URIs and the request parser used to throw that information away. Using ``urlparse.urlsplit()``, all pieces of the URL are recorded. - The proxy acheme and netloc/hostname are exposed in the WSGI environment as ``zserver.proxy.scheme`` and ``zserver.proxy.host``. - Made tests runnable via buildout again. 3.6.2 (2010-06-11) ------------------ - The log message "Exception during task" is no longer logged to the root logger but to zope.server.taskthreads. 3.6.1 (2009-10-07) ------------------ - Made tests pass with current zope.publisher which restricts redirects to the current host by default. 3.6.0 (2009-05-27) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner - Removed unused dependency on zope.deprecation. - Remove old zpkg-related DEPENDENCIES.cfg file. 3.5.0 (2008-03-01) ------------------ - Improve package meta-data. - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html - Removed dependency on ZODB. 3.5.0a2 (2007-06-02) -------------------- - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.5.0a1 (2007-06-02) -------------------- - Added a factory and entry point for PasteDeploy. 3.4.3 (2008-08-18) ------------------ - Moved some imports from test modules to their setUp to prevent failures when ZEO tests are run by the same testrunner 3.4.2 (2008-02-02) ------------------ - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html 3.4.1 (2007-06-02) ------------------ - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. 3.4.0 (2007-06-02) ------------------ - Removed an unused import. Unchanged otherwise. 3.4.0a1 (2007-04-22) -------------------- - Initial release as a separate project, corresponds to zope.server from Zope 3.4.0a1 - Made WSGI server really WSGI-compliant by adding variables to the environment that are required by the spec. Keywords: zope3 server http ftp Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Framework :: Zope3 zope.server-3.8.6/test.ini0000664000175000017500000000022111701734341014520 0ustar mgmg00000000000000[app:main] paste.app_factory = zope.server.http.tests.wsgi_app:test_app_factory [server:main] use = egg:zope.server host = 127.0.0.1 port = 8080zope.server-3.8.6/setup.cfg0000664000175000017500000000007311701734350014666 0ustar mgmg00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0