Commit c658c172 authored by Fred Drake's avatar Fred Drake

Move ZServer into new location, including configuration support from the

new-install-branch.
parent 5aa6b549
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
A logging module which handles ZServer access log messages.
This depends on Vinay Sajip's PEP 282 logging module.
"""
import logging
from zLOG.BaseLogger import BaseLogger
class AccessLogger(BaseLogger):
logger = logging.getLogger('access')
def log(self, message):
if not self.logger.handlers: # dont log if we have no handlers
return
if message.endswith('\n'):
message = message[:-1]
self.logger.warn(message)
access_logger = AccessLogger()
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Logs debugging information about how ZServer is handling requests
and responses. This log can be used to help locate troublesome requests.
The format of a log message is:
<code> <request id> <time> <data>
where:
<code> is B for begin, I for received input, A for received output,
E for sent output.
<request id> is a unique request id.
<time> is the local time in ISO 6801 format.
<data> is the HTTP method and the PATH INFO for B, the size of the
input for I, the HTTP status code and the size of the output for
A, or nothing for E.
"""
import time
import logging
from zLOG.BaseLogger import BaseLogger
class DebugLogger(BaseLogger):
logger = logging.getLogger('trace')
def log(self, code, request_id, data=''):
if not self.logger.handlers:
return
# Omitting the second parameter requires Python 2.2 or newer.
t = time.strftime('%Y-%m-%dT%H:%M:%S')
message = '%s %s %s %s' % (code, request_id, t, data)
self.logger.warn(message)
debug_logger = DebugLogger()
log = debug_logger.log
reopen = debug_logger.reopen
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
FTP Request class for FTP server.
The FTP Request does the dirty work of turning an FTP request into something
that ZPublisher can understand.
"""
from ZPublisher.HTTPRequest import HTTPRequest
from cStringIO import StringIO
import os
from base64 import encodestring
import re
class FTPRequest(HTTPRequest):
def __init__(self, path, command, channel, response, stdin=None,
environ=None,globbing=None,recursive=0):
# we need to store the globbing information to pass it
# to the ZPublisher and the manage_FTPlist function
# (ajung)
self.globbing = globbing
self.recursive= recursive
if stdin is None: stdin=StringIO()
if environ is None:
environ=self._get_env(path, command, channel, stdin)
self._orig_env=environ
HTTPRequest.__init__(self, stdin, environ, response, clean=1)
# support for cookies and cookie authentication
self.cookies=channel.cookies
if not self.cookies.has_key('__ac') and channel.userid != 'anonymous':
self.other['__ac_name']=channel.userid
self.other['__ac_password']=channel.password
for k,v in self.cookies.items():
if not self.other.has_key(k):
self.other[k]=v
def retry(self):
self.retry_count=self.retry_count+1
r=self.__class__(stdin=self.stdin,
environ=self._orig_env,
response=self.response.retry(),
channel=self, # For my cookies
)
return r
def _get_env(self, path, command, channel, stdin):
"Returns a CGI style environment"
env={}
env['SCRIPT_NAME']='/%s' % channel.module
env['REQUEST_METHOD']='GET' # XXX what should this be?
env['SERVER_SOFTWARE']=channel.server.SERVER_IDENT
if channel.userid != 'anonymous':
env['HTTP_AUTHORIZATION']='Basic %s' % re.sub('\012','',
encodestring('%s:%s' % (channel.userid, channel.password)))
env['SERVER_NAME']=channel.server.hostname
env['SERVER_PORT']=str(channel.server.port)
env['REMOTE_ADDR']=channel.client_addr[0]
env['GATEWAY_INTERFACE']='CGI/1.1' # that's stretching it ;-)
# FTP commands
#
if type(command)==type(()):
args=command[1:]
command=command[0]
if command in ('LST','CWD','PASS'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPlist')
elif command in ('MDTM','SIZE'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPstat')
elif command=='RETR':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_FTPget')
elif command in ('RMD','DELE'):
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_delObjects')
env['QUERY_STRING']='ids=%s' % args[0]
elif command=='MKD':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_addFolder')
env['QUERY_STRING']='id=%s' % args[0]
elif command=='RNTO':
env['PATH_INFO']=self._join_paths(channel.path,
path, 'manage_renameObject')
env['QUERY_STRING']='id=%s&new_id=%s' % (args[0],args[1])
elif command=='STOR':
env['PATH_INFO']=self._join_paths(channel.path, path)
env['REQUEST_METHOD']='PUT'
env['CONTENT_LENGTH']=len(stdin.getvalue())
else:
env['PATH_INFO']=self._join_paths(channel.path, path, command)
# Fake in globbing information
env['GLOBBING'] = self.globbing
env['FTP_RECURSIVE'] = self.recursive
return env
def _join_paths(self,*args):
path=apply(os.path.join,args)
path=os.path.normpath(path)
if os.sep != '/':
path=path.replace(os.sep,'/')
return path
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Response class for the FTP Server.
"""
from ZServer.HTTPResponse import ZServerHTTPResponse
from PubCore.ZEvent import Wakeup
from cStringIO import StringIO
import marshal
class FTPResponse(ZServerHTTPResponse):
"""
Response to an FTP command
"""
def __str__(self):
# return ZServerHTTPResponse.__str__(self)
# ZServerHTTPResponse.__str__(self) return HTTP headers
# Why should be send them to the FTP client ??? (ajung)
return ''
def outputBody(self):
pass
def setCookie(self, name, value, **kw):
self.cookies[name]=value
def appendCookie(self, name, value):
self.cookies[name]=self.cookies[name] + value
def expireCookie(self, name, **kw):
if self.cookies.has_key(name):
del self.cookies[name]
def _cookie_list(self):
return []
def _marshalledBody(self):
return marshal.loads(self.body)
def setMessage(self, message):
self._message = message
def getMessage(self):
return getattr(self, '_message', '')
class CallbackPipe:
"""
Sends response object to a callback. Doesn't write anything.
The callback takes place in Medusa's thread, not the request thread.
"""
def __init__(self, callback, args):
self._callback=callback
self._args=args
self._producers=[]
def close(self):
pass
def write(self, text, l=None):
if text:
self._producers.append(text)
def finish(self, response):
self._response=response
Wakeup(self.apply) # move callback to medusas thread
def apply(self):
result=apply(self._callback, self._args+(self._response,))
# break cycles
self._callback=None
self._response=None
self._args=None
return result
def make_response(channel, callback, *args):
# XXX should this be the FTPResponse constructor instead?
r=FTPResponse(stdout=CallbackPipe(callback, args), stderr=StringIO())
r.setHeader('content-type','text/plain')
r.cookies=channel.cookies
return r
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
# Medusa ICP server
#
# Why would you want to use this?
# see http://www.zope.org/Members/htrd/icp/intro
import sys, string, os, socket, errno, struct
import asyncore
from medusa import counter
ICP_OP_QUERY = 1
ICP_OP_HIT = 2
ICP_OP_MISS = 3
ICP_OP_ERR = 4
ICP_OP_MISS_NOFETCH = 21
ICP_OP_DENIED = 22
class BaseICPServer(asyncore.dispatcher):
REQUESTS_PER_LOOP = 4
_shutdown = 0
def __init__ (self,ip,port):
asyncore.dispatcher.__init__(self)
self.create_socket (socket.AF_INET, socket.SOCK_DGRAM)
self.set_reuse_addr()
self.bind((ip,port))
if ip=='':
addr = 'any'
else:
addr = ip
self.log_info('ICP server started\n\tAddress: %s\n\tPort: %s' % (addr,port) )
def clean_shutdown_control(self,phase,time_in_this_phase):
if phase==1:
# Stop responding to requests.
if not self._shutdown:
self._shutdown = 1
self.log_info('shutting down ICP')
if time_in_this_phase<2.0:
# We have not yet been deaf long enough for our front end proxies to notice.
# Do not allow shutdown to proceed yet
return 1
else:
# Shutdown can proceed. We dont need a socket any more
self.close()
return 0
def handle_read(self):
for i in range(self.REQUESTS_PER_LOOP):
try:
request, whence = self.socket.recvfrom(16384)
except socket.error,e:
if e[0]==errno.EWOULDBLOCK:
break
else:
raise
else:
if self.check_whence(whence):
reply = self.calc_reply(request)
if reply:
self.socket.sendto(reply,whence)
def readable(self):
return not self._shutdown
def writable(self):
return 0
def handle_write (self):
self.log_info ('unexpected write event', 'warning')
def handle_error (self): # don't close the socket on error
(file,fun,line), t, v, tbinfo = asyncore.compact_traceback()
self.log_info('Problem in ICP (%s:%s %s)' % (t, v, tbinfo),
'error')
def check_whence(self,whence):
return 1
def calc_reply(self,request):
if len(request)>20:
opcode,version,length,number,options,opdata,junk = struct.unpack('!BBHIIII',request[:20])
if version==2:
if opcode==ICP_OP_QUERY:
if len(request)!=length:
out_opcode = ICP_OP_ERR
else:
url = request[24:]
if url[-1:]=='\x00':
url = url[:-1]
out_opcode = self.check_url(url)
return struct.pack('!BBHIIII',out_opcode,2,20,number,0,0,0)
def check_url(self,url):
# derived classes replace this with a more
# useful policy
return ICP_OP_MISS
class ICPServer(BaseICPServer):
# Products that want to do special ICP handling should .append their hooks into
# this list. Each hook is called in turn with the URL as a parameter, and
# they must return an ICP_OP code from above or None. The first
# non-None return is used as the ICP response
hooks = []
def check_url(self,url):
for hook in self.hooks:
r = hook(url)
if r is not None:
return r
return ICP_OP_MISS
ZServer Installation
--------------------
Requirements
ZServer comes with Zope 2. Though ZServer can be used with earlier
versions of Zope, this is not supported and not covered by this
document.
Configuration
To run ZServer you simply execute the z2.py start script which is
located in your Zope directory. You can pass commandline arguments
to the start script in order to run Zope with different options. In
order to see a list of options use the '-h' help option.
Here's an example of use::
$ python2.1 z2.py -w 8888 -f "" -p "" -m "" &
This example starts Zope using a web server on port 8888. It does
not start and FTP server, or a PCGI server, or a Monitor server. It
also starts the server running in the backaground.
Shell scripts and batch files
You may also wish to create a shell script (or batch file under
win32) to set environment variables (such as ZOPE_DEBUG_MODE and
PYTHONHOME) and run the start script.
Here's an example shell script for a binary Zope release::
ZOPE_DEBUG_MODE=1
export ZOPE_DEBUG_MODE
PYTHONHOME=/home/Zope
export PYTHONHOME
/home/Zope/bin/python /home/Zope/z2.py -w 9673 &
Note: If ZServer fails because it can't find some standard Python
libaries there's a good bet that you need to set the PYTHONHOME as
shown above.
Here's an example batch file::
set ZOPE_DEBUG_MODE=1
"\Program Files\Zope\bin\python" "\Program Files\Zope\z2.py -w
8888 -f 8021"
Now you're ready to go.
Starting ZServer
To start ZServer run the start script::
$ python2.1 z2.py
To stop the server type 'control-c'.
Note: If you've created a shell script or batch file to run ZServer
use that instead.
You should see some Medusa information come up on the screen as Zope
starts.
A log file will be written to the 'var' directory, named
'Z2.log' by default.
Using ZServer
Once you start ZServer is will publish Zope (or any Python module)
on HTTP and/or FTP. To access Zope via HTTP point your browser at
the server like so::
http://www.example.com:9673/
This assumes that you have chosen to put HTTP on port 9673 and that
you are publishing a module named whose URL prefix is set to ''.
Note: to publish Zope normally you publish the 'lib/python/Zope.py'
module.
To access Zope via FTP you need to FTP to it at the port you set FTP
to run on. For example::
ftp www.example.com 9221
This opens a FTP session to your machine on port 9221, ZServer's
default FTP port. When you are prompted to log in you should supply
a Zope username and password. (Probably you should use an account
with the 'Manager' role, unless you have configured Zope to allow
FTP access to the 'Anonymous' role.) You can also enter 'anonymous'
and any password for anonymous FTP access. Once you have logged in
you can start issuing normal FTP commands.
Right now ZServer supports most basic FTP commands.
Note: When you log in your working directory is set to '/'. If you
do not have FTP permissions in this directory, you will need to 'cd'
to a directory where you have permissions before you can do
anything. See above for more information about logging into FTP.
Advanced Usage: zdaemon.py and the Zope NT service.
One problem you may notice with ZServer is that once the server is
shutdown, either through the web management interface, or by some
other means, it will not automatically be restarted.
On Unix you can use zdeamon.py to keep Zope running. Specifying
the '-Z' switch when starting Zope runs zdaemon.py. Zdeamon
will restart Zope when it Zope is restarted through the web, and in
case of an unexpected error.
On NT, use the Zope service for the same functionality. See ZServer.py
for more information on running ZServer as a service.
Where to go from here
Check out the README.txt file. It contains information on what
ZServer can do, how it works and and what you can do if you run into
problems.
And don't forget to have fun!
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer pipe utils. These producers basically function as callbacks.
"""
import asyncore
import sys
class ShutdownProducer:
"shuts down medusa"
def more(self):
asyncore.close_all()
class LoggingProducer:
"logs request"
def __init__(self, logger, bytes, method='log'):
self.logger=logger
self.bytes=bytes
self.method=method
def more(self):
getattr(self.logger, self.method)(self.bytes)
self.logger=None
return ''
class CallbackProducer:
"Performs a callback in the channel's thread"
def __init__(self, callback):
self.callback=callback
def more(self):
self.callback()
self.callback=None
return ''
class file_part_producer:
"producer wrapper for part of a file[-like] objects"
# match http_channel's outgoing buffer size
out_buffer_size = 1<<16
def __init__(self, file, lock, start, end):
self.file=file
self.lock=lock
self.start=start
self.end=end
def more(self):
end=self.end
if not end: return ''
start=self.start
if start >= end: return ''
file=self.file
size=end-start
bsize=self.out_buffer_size
if size > bsize: size=bsize
self.lock.acquire()
try:
file.seek(start)
data = file.read(size)
finally:
self.lock.release()
if data:
start=start+len(data)
if start < end:
self.start=start
return data
self.end=0
del self.file
return data
class file_close_producer:
def __init__(self, file):
self.file=file
def more(self):
file=self.file
if file is not None:
file.close()
self.file=None
return ''
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Simple Event Manager Based on Pipes
"""
from ZServer.medusa.thread.select_trigger import trigger
from asyncore import socket_map
class simple_trigger(trigger):
def handle_close(self):
pass
the_trigger=simple_trigger()
def Wakeup(thunk=None):
global the_trigger
try:
the_trigger.pull_trigger(thunk)
except OSError, why:
# this broken pipe as a result of perhaps a signal
# we want to handle this gracefully so we get rid of the old
# trigger and install a new one.
if why[0] == 32:
del socket_map[the_trigger._fileno]
the_trigger = simple_trigger() # adds itself back into socket_map
the_trigger.pull_trigger(thunk)
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import thread
from ZServerPublisher import ZServerPublisher
class ZRendevous:
def __init__(self, n=1):
sync=thread.allocate_lock()
self._a=sync.acquire
self._r=sync.release
pool=[]
self._lists=pool, [], []
self._a()
try:
while n > 0:
l=thread.allocate_lock()
l.acquire()
pool.append(l)
thread.start_new_thread(ZServerPublisher,
(self.accept,))
n=n-1
finally: self._r()
def accept(self):
self._a()
try:
pool, requests, ready = self._lists
while not requests:
l=pool[-1]
del pool[-1]
ready.append(l)
self._r()
l.acquire()
self._a()
pool.append(l)
r=requests[0]
del requests[0]
return r
finally: self._r()
def handle(self, name, request, response):
self._a()
try:
pool, requests, ready = self._lists
requests.append((name, request, response))
if ready:
l=ready[-1]
del ready[-1]
l.release()
finally: self._r()
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
from ZPublisher import publish_module
class ZServerPublisher:
def __init__(self, accept):
while 1:
try:
name, request, response=accept()
publish_module(
name,
request=request,
response=response)
finally:
response._finish()
request=response=None
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import ZRendezvous
_handle=None
_n=1
def handle(*args, **kw):
global _handle
if _handle is None: _handle=ZRendezvous.ZRendevous(_n).handle
return apply(_handle, args, kw)
def setNumberOfThreads(n):
global _n
_n=n
global setNumberOfThreads
del setNumberOfThreads
ZServer README
--------------
What is ZServer?
ZServer is an integration of the Zope application server and the
Medusa information server. See the ZServer architecture document for
more information::
http://www.zope.org/Documentation/Reference/ZServer
ZServer gives you HTTP, FTP, WebDAV, PCGI, and remote interactive
Python access. In later releases it will probably offer more
protocols such as FastCGI, etc.
What is Medusa?
Medusa is a Python server framework with uses a single threaded
asynchronous sockets approach. For more information see::
http://www.nightmare.com/medusa
There's also an interesting Medusa tutorial at::
http://www.nightmare.com:8080/nm/apps/medusa/docs/programming.html
ZServer HTTP support
ZServer offers HTTP 1.1 publishing for Zope. It does not support
publishing files from the file system. You can specify the HTTP port
using the -w command line argument for the z2.py start script. You
can also specify CGI environment variables on the command line using
z2.py
ZServer FTP support
What you can do with FTP
FTP access to Zope allows you to FTP to the Zope object hierarchy
in order to perform managerial tasks. You can:
* Navigate the object hierarchy with 'cd'
* Replace the content of Documents, Images, and Files
* Create Documents, Images, Files, Folders
* Delete objects and Folders.
So basically you can do more than is possible with HTTP PUT. Also,
unlike PUT, FTP gives you access to Document content. So when you
download a Document you are getting its content, not what it looks
like when it is rendered.
Using FTP
To FTP into Zope, ZServer must be configured to serve FTP. By
default ZServer serves FTP on port 9221. So to connect to Zope you
would issue a command like so::
$ ftp localhost 9221
When logging in to FTP, you have some choices. You can connect
anonymously by using a username of 'anonymous' and any password.
Or you can login as a Zope user. Since Zope users are defined at
different locations in the object hierarchy, authentication can be
problematic. There are two solutions:
* login and then cd to the directory where you are defined.
* login with a special name that indicates where you are
defined.
The format of the special name is '<username>@<path>'. For
example::
joe@Marketing/Projects
FTP permissions
FTP support is provided for Folders, Documents, Images, and Files.
You can control access to FTP via the new 'FTP access' permission.
This permission controls the ability to 'cd' to a Folder and to
download objects. Uploading and deleting and creating objects are
controlled by existing permissions.
FTP limits
You can set limits for the number of simultaneous FTP connections.
You can separately configure the number of anonymous and
authenticated connections. Right now this setting is set in
'ZServerFTP.py'. In the future, it may be more easy to configure.
Properties and FTP: The next step
The next phase of FTP support will allow you to edit properties of
all Zope objects. Probably properties will be exposed via special
files which will contain an XML representation of the object's
properties. You could then download the file, edit the XML and
upload it to change the object's properties.
We do not currently have a target date for FTP property support.
How does FTP work?
The ZServer's FTP channel object translates FTP requests into
ZPublisher requests. The FTP channel then analyses the response
and formulates an appropriate FTP response. The FTP channel
stores some state such as the current working directory and the
username and password.
On the Zope side of things, the 'lib/python/OFS/FTPInterface.py'
module defines the Zope FTP interface, for listing sub-items,
stating, and getting content. The interface is implemented in
'SimpleItem', and in other Zope classes. Programmers will not
need to implement the entire interface if they inherit from
'SimpleItem'. All the other FTP functions are handled by
existing methods like 'manage_delObjects', and 'PUT', etc.
ZServer PCGI support
ZServer will service PCGI requests with both inet and unix domain
sockets. This means you can use ZServer instead of
'pcgi_publisher.py' as your long running PCGI server process. In the
future, PCGI may be able to activate ZServer.
Using PCGI instead of HTTP allows you to forward requests from
another web server to ZServer. The CGI environment and HTTP headers
are controlled by the web server, so you don't need to worry about
managing the ZServer environment. However, this configuration will
impose a larger overhead than simply using the web server as an HTTP
proxy for ZServer.
To use PCGI, configure your PCGI info files to communicate with
ZServer by setting the PCGI_PORT, PCGI_SOCKET_FILE, and PCGI_NAME.
The other PCGI settings are currently ignored by ZServer.
ZServer's PCGI support will work with mod_pcgi.
ZServer monitor server
ZServer now includes the Medusa monitor server. This basically gives
you a remote, secure Python prompt. You can interactively access Zope.
This is a very powerful, but dangerous tool. Be careful.
To use the monitor server specify a monitor port number using the -m
option with the z2.py start script. The default port is 9999.
To connect to the monitor server use the 'ZServer/medusa/monitor_client.py'
or 'ZServer/medusa/monitor_client_win32.py' script. For example::
$ python2.1 ZServer/medusa/monitor_client.py localhost 9999
You will then be asked to enter a password. This is the Zope super manager
password which is stored in the 'access' file.
Then you will be greeted with a Python prompt. To access Zope import
the Zope module::
>>> import Zope
The Zope top level Zope object is available via the 'Zope.app' function::
>>> a=Zope.app()
From this object you can reach all other Zope objects as subobjects.
Remember if you make changes to Zope objects and want those changes to be
saved you need to commmit the transaction::
>>> get_transaction().commit()
ZServer WebDAV support
WebDAV is a new protocol for managing web resources. WebDAV operates
over HTTP. Since WebDAV uses HTTP, ZServer doesn't really have to do
anything special, except stay out of Zope's way when handling WebDAV
requests.
The only major WebDAV client at this time is Internet Explorer 5. It
works with Zope.
Differences between ZopeHTTPServer and ZServer
ZopeHTTPServer is old and no longer being actively maintained.
Both ZopeHTTPServer and ZServer are Python HTTP servers.
ZopeHTTPServer is built on the standard Python SimpleHTTPServer
framework. ZServer is built on Medusa.
ZopeHTTPServer is very limited. It can only publish one module at a
time. It can only publish via HTTP. It has no support for thread
pools.
ZServer on the other hand is more complex and supports publishing
multiple modules, thread pools, and it uses a new threaded
architecture for accessing ZPublisher.
Running ZServer as nobody
Normally ZServer will run with the userid of the user who starts
it. However, if ZServer is started by root, it will attempt to
become nobody or any userid you specify with the -u argument to the
z2.py start script.
ZServer is similar to ZopeHTTPServer in these respects.
If you run Zope with different userids you must be aware of
permission issues. Zope must be able to read and write to the 'var'
directory. If you change the userid Zope is running under you will
probably need to change the permissions on the 'var' directory
and the files in it in order for Zope to run under a different
userid.
Support
Questions and comments should go to 'support@digicool.com'.
You can report bugs and check on the status of bugs using the Zope
bug collector::
http://www.zope.org/Resources/Collector/
License
ZServer is covered by the ZPL despite the fact that it comes with
much of the Medusa source code. The portions of Medusa that come
with ZServer are licensed under the ZPL.
Outstanding issues
The FTP interface for Zope objects may be changed.
HTTP 1.1 support is ZServer is incomplete, though it should work for
most HTTP 1.1 clients.
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""HTTP handler which forces GET requests to return the document source.
Works around current WebDAV clients' failure to implement the
'source-link' feature of the specification. Uses manage_FTPget().
"""
import os
import posixpath
from ZServer.HTTPServer import zhttp_handler
__version__ = "1.0"
class WebDAVSrcHandler(zhttp_handler):
def get_environment(self, request):
"""Munge the request to ensure that we call manage_FTPGet."""
env = zhttp_handler.get_environment(self, request)
# Set a flag to indicate this request came through the WebDAV source
# port server.
env['WEBDAV_SOURCE_PORT'] = 1
if env['REQUEST_METHOD'] == 'GET':
path_info = env['PATH_INFO']
if os.sep != '/':
path_info = path_info.replace(os.sep, '/')
path_info = posixpath.join(path_info, 'manage_FTPget')
path_info = posixpath.normpath(path_info)
env['PATH_INFO'] = path_info
# Workaround for lousy WebDAV implementation of M$ Office 2K.
# Requests for "index_html" are *sometimes* send as "index_html."
# We check the user-agent and remove a trailing dot for PATH_INFO
# and PATH_TRANSLATED
if env.get("HTTP_USER_AGENT", "").find(
"Microsoft Data Access Internet Publishing Provider") > -1:
if env["PATH_INFO"][-1] == '.':
env["PATH_INFO"] = env["PATH_INFO"][:-1]
if env["PATH_TRANSLATED"][-1] == '.':
env["PATH_TRANSLATED"] = env["PATH_TRANSLATED"][:-1]
return env
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
ZServer as a NT service.
The serice starts up and monitors a ZServer process.
Features:
* When you start the service it starts ZServer
* When you stop the serivice it stops ZServer
* It monitors ZServer and restarts it if it exits abnormally
* If ZServer is shutdown from the web, the service stops.
* If ZServer cannot be restarted, the service stops.
Usage:
Installation
The ZServer service should be installed by the Zope Windows
installer. You can manually install, uninstall the service from
the commandline.
ZService.py [options] install|update|remove|start [...]
|stop|restart [...]|debug [...]
Options for 'install' and 'update' commands only:
--username domain\username : The Username the service is to run
under
--password password : The password for the username
--startup [manual|auto|disabled] : How the service starts,
default = manual
Commands
install : Installs the service
update : Updates the service, use this when you change
ZServer.py
remove : Removes the service
start : Starts the service, this can also be done from the
services control panel
stop : Stops the service, this can also be done from the
services control panel
restart : Restarts the service
debug : Runs the service in debug mode
You can view the usage options by running ZServer.py without any
arguments.
Note: you may have to register the Python service program first,
win32\pythonservice.exe /register
Starting Zope
Start Zope by clicking the 'start' button in the services control
panel. You can set Zope to automatically start at boot time by
choosing 'Auto' startup by clicking the 'statup' button.
Stopping Zope
Stop Zope by clicking the 'stop' button in the services control
panel. You can also stop Zope through the web by going to the
Zope control panel and by clicking 'Shutdown'.
Event logging
Zope events are logged to the NT application event log. Use the
event viewer to keep track of Zope events.
Registry Settings
You can change how the service starts ZServer by editing a registry
key.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
<Service Name>\Parameters\start
The value of this key is the command which the service uses to
start ZServer. For example:
"C:\Program Files\Zope\bin\python.exe"
"C:\Program Files\Zope\z2.py" -w 8888
TODO:
* Integrate it into the Windows installer.
* Add ZLOG logging in addition to event log logging.
* Make it easier to run multiple Zope services with one Zope install
This script does for NT the same sort of thing zdaemon.py does for UNIX.
Requires Python win32api extensions.
"""
import sys, os, time, imp
# Some fancy path footwork is required here because we
# may be run from python.exe or lib/win32/PythonService.exe
home=os.path.split(os.path.split(sys.executable)[0])[0]
if sys.executable[-10:]!='python.exe':
home=os.path.split(home)[0]
home=os.path.split(home)[0]
sys.path.append(os.path.join(home, 'bin'))
sys.path.append(os.path.join(home, 'ZServer'))
sys.path.append(os.path.join(home, 'bin', 'lib', 'win32'))
sys.path.append(os.path.join(home, 'bin', 'lib', 'win32', 'lib'))
# pythoncom and pywintypes are special, and require these hacks when
# we dont have a standard Python installation around.
import win32api
def magic_import(modulename, filename):
# by Mark Hammond
try:
# See if it does import first!
return __import__(modulename)
except ImportError:
pass
# win32 can find the DLL name.
h = win32api.LoadLibrary(filename)
found = win32api.GetModuleFileName(h)
# Python can load the module
mod = imp.load_module(modulename, None, found, ('.dll', 'rb', imp.C_EXTENSION))
# inject it into the global module list.
sys.modules[modulename] = mod
# And finally inject it into the namespace.
globals()[modulename] = mod
win32api.FreeLibrary(h)
magic_import('pywintypes','pywintypes21.dll')
import win32serviceutil, win32service, win32event, win32process
try: import servicemanager
except: pass
class ZServerService(win32serviceutil.ServiceFramework):
# Some trickery to determine the service name. The WISE
# installer will write an svcname.txt to the ZServer dir
# that we can use to figure out our service name.
path=os.path.join(home, 'ZServer', 'svcname.txt')
file=open(path, 'r')
_svc_name_=file.readline().strip()
file.close()
_svc_display_name_ = "Zope (%s)" % _svc_name_
restart_min_time=5 # if ZServer restarts before this many
# seconds then we have a problem, and
# need to stop the service.
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def SvcDoRun(self):
self.start_zserver()
while 1:
rc=win32event.WaitForMultipleObjects(
(self.hWaitStop, self.hZServer), 0, win32event.INFINITE)
if rc - win32event.WAIT_OBJECT_0 == 0:
break
else:
self.restart_zserver()
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING, 5000)
def SvcStop(self):
servicemanager.LogInfoMsg('Stopping Zope.')
try:
self.stop_zserver()
except:
pass
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
def start_zserver(self):
sc=self.get_start_command()
result=win32process.CreateProcess(None, self.get_start_command(),
None, None, 0, 0, None, None, win32process.STARTUPINFO())
self.hZServer=result[0]
self.last_start_time=time.time()
servicemanager.LogInfoMsg('Starting Zope.')
def stop_zserver(self):
win32process.TerminateProcess(self.hZServer,0)
def restart_zserver(self):
if time.time() - self.last_start_time < self.restart_min_time:
servicemanager.LogErrorMsg('Zope died and could not be restarted.')
self.SvcStop()
code=win32process.GetExitCodeProcess(self.hZServer)
if code == 0:
# Exited with a normal status code,
# assume that shutdown is intentional.
self.SvcStop()
else:
servicemanager.LogWarningMsg('Restarting Zope.')
self.start_zserver()
def get_start_command(self):
return win32serviceutil.GetServiceCustomOption(self,'start')
def set_start_command(value):
"sets the ZServer start command if the start command is not already set"
current=win32serviceutil.GetServiceCustomOption(ZServerService,
'start', None)
if current is None:
win32serviceutil.SetServiceCustomOption(ZServerService,'start',value)
if __name__=='__main__':
win32serviceutil.HandleCommandLine(ZServerService)
if 'install' in sys.argv:
command='"%s" "%s" -S' % (sys.executable, os.path.join(home,'z2.py'))
set_start_command(command)
print "Setting ZServer start command to:", command
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import sys
from medusa.test import max_sockets
CONNECTION_LIMIT=max_sockets.max_select_sockets()
ZSERVER_VERSION='1.1'
try:
import App.version_txt
ZOPE_VERSION=App.version_txt.version_txt()
except:
ZOPE_VERSION='experimental'
exit_code = 0
# Try to poke zLOG default logging into asyncore
# XXX We should probably should do a better job of this,
# however that would mean that ZServer required zLOG.
# (Is that really a bad thing?)
try:
from zLOG import LOG, register_subsystem, BLATHER, INFO, WARNING, ERROR
except ImportError:
pass
else:
register_subsystem('ZServer')
severity={'info':INFO, 'warning':WARNING, 'error': ERROR}
def log_info(self, message, type='info'):
if message[:14]=='adding channel' or \
message[:15]=='closing channel' or \
message == 'Computing default hostname':
LOG('ZServer', BLATHER, message)
else:
LOG('ZServer', severity[type], message)
import asyncore
asyncore.dispatcher.log_info=log_info
# A routine to try to arrange for request sockets to be closed
# on exec. This makes it easier for folks who spawn long running
# processes from Zope code. Thanks to Dieter Maurer for this.
try:
import fcntl
def requestCloseOnExec(sock):
try:
fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)
except: # XXX What was this supposed to catch?
pass
except (ImportError, AttributeError):
def requestCloseOnExec(sock):
pass
import asyncore
from medusa import resolver, logger
from HTTPServer import zhttp_server, zhttp_handler
from PCGIServer import PCGIServer
from FCGIServer import FCGIServer
from FTPServer import FTPServer
from PubCore import setNumberOfThreads
from medusa.monitor import secure_monitor_server
# override the service name in logger.syslog_logger
logger.syslog_logger.svc_name='ZServer'
<component prefix="ZServer.datatypes">
<abstracttype name="server">
<description>
The "server" type is used to describe a single type of server
instance. The value for a server section is an object with the
ServerFactory interface.
</description>
</abstracttype>
<sectiontype name="http-server"
datatype=".HTTPServerFactory"
implements="server">
<key name="address" datatype="inet-address"/>
<key name="force-connection-close" datatype="boolean" default="off"/>
<key name="webdav-source-clients">
<description>
Regular expression used to identify clients who should
receive WebDAV source responses to GET requests.
</description>
</key>
</sectiontype>
<sectiontype name="webdav-source-server"
datatype=".WebDAVSourceServerFactory"
implements="server">
<key name="address" datatype="inet-address"/>
<key name="force-connection-close" datatype="boolean" default="off"/>
</sectiontype>
<sectiontype name="persistent-cgi"
datatype=".PCGIServerFactory"
implements="server">
<key name="path" datatype="existing-file"/>
</sectiontype>
<sectiontype name="fast-cgi"
datatype=".FCGIServerFactory"
implements="server">
<key name="address" datatype="socket-address"/>
</sectiontype>
<sectiontype name="ftp-server"
datatype=".FTPServerFactory"
implements="server">
<key name="address" datatype="inet-address"/>
</sectiontype>
<sectiontype name="monitor-server"
datatype=".MonitorServerFactory"
implements="server">
<key name="address" datatype="inet-address"/>
</sectiontype>
<sectiontype name="icp-server"
datatype=".ICPServerFactory"
implements="server">
<key name="address" datatype="inet-address"/>
</sectiontype>
</component>
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""ZConfig datatype support for ZServer.
Each server type is represented by a ServerFactory instance.
"""
class ServerFactory:
def __init__(self, address=None):
if address is None:
self.host = None
self.port = None
else:
self.host, self.port = address
def prepare(self, defaulthost=None, dnsresolver=None,
module=None, env=None, portbase=None):
if defaulthost is not None:
self._set_default_host(defaulthost)
self.dnsresolver = dnsresolver
self.module = module
self.cgienv = env
if portbase and self.port is not None:
self.port += portbase
def _set_default_host(self, host):
if not self.host:
self.host = host
def servertype(self):
s = self.__class__.__name__
if s.endswith("Factory"):
s = s[:-7]
return s
def create(self):
raise NotImplementedError(
"Concrete ServerFactory classes must implement create().")
class HTTPServerFactory(ServerFactory):
def __init__(self, section):
ServerFactory.__init__(self, section.address)
self.force_connection_close = section.force_connection_close
# webdav-source-server sections won't have webdav_source_clients:
webdav_clients = getattr(section, "webdav_source_clients", None)
self.webdav_source_clients = webdav_clients
def create(self):
from ZServer import HTTPServer
from ZServer.AccessLogger import access_logger
handler = self.createHandler()
handler._force_connection_close = self.force_connection_close
if self.webdav_source_clients:
handler.set_webdav_source_clients(self.webdav_source_clients)
server = HTTPServer.zhttp_server(ip=self.host, port=self.port,
resolver=self.dnsresolver,
logger_object=access_logger)
server.install_handler(handler)
return server
def createHandler(self):
from ZServer import HTTPServer
return HTTPServer.zhttp_handler(self.module, '', self.cgienv)
class WebDAVSourceServerFactory(HTTPServerFactory):
def createHandler(self):
from ZServer.WebDAVSrcHandler import WebDAVSrcHandler
return WebDAVSrcHandler(self.module, '', self.cgienv)
class FTPServerFactory(ServerFactory):
def __init__(self, section):
ServerFactory.__init__(self, section.address)
def create(self):
from ZServer.AccessLogger import access_logger
from ZServer.FTPServer import FTPServer
return FTPServer(ip=self.host, port=self.port,
module=self.module, resolver=self.dnsresolver,
logger_object=access_logger)
class PCGIServerFactory(ServerFactory):
def __init__(self, section):
ServerFactory.__init__(self)
self.path = section.path
def create(self):
from ZServer.AccessLogger import access_logger
from ZServer.PCGIServer import PCGIServer
return PCGIServer(ip=self.host, port=self.port,
module=self.module, resolver=self.dnsresolver,
pcgi_file=self.path,
logger_object=access_logger)
class FCGIServerFactory(ServerFactory):
def __init__(self, section):
import socket
if section.address.family == socket.AF_INET:
address = section.address.address
path = None
else:
address = None
path = section.address.address
ServerFactory.__init__(self, address)
self.path = path
def _set_default_host(self, host):
if self.path is None:
ServerFactory._set_default_host(self, host)
def create(self):
from ZServer.AccessLogger import access_logger
from ZServer.FCGIServer import FCGIServer
return FCGIServer(ip=self.host, port=self.port,
socket_file=self.path,
module=self.module, resolver=self.dnsresolver,
logger_object=access_logger)
class MonitorServerFactory(ServerFactory):
def __init__(self, section):
ServerFactory.__init__(self, section.address)
def create(self):
from ZServer.medusa.monitor import secure_monitor_server
return secure_monitor_server(hostname=self.host, port=self.port,
password=self.getPassword())
def getPassword(self):
# XXX This is really out of place; there should be a better
# way. For now, at least we can make it a separate method.
import ZODB # :-( required to import user
from AccessControl.User import emergency_user
if hasattr(emergency_user, '__null_user__'):
pw = None
else:
pw = emergency_user._getPassword()
if pw is None:
import zLOG
zLOG.LOG("Zope", zLOG.WARNING, 'Monitor server not started'
' because no emergency user exists.')
return pw
class ICPServerFactory(ServerFactory):
def __init__(self, section):
ServerFactory.__init__(self, section.address)
def create(self):
from ZServer.ICPServer import ICPServer
return ICPServer(self.host, self.port)
# Make medusa into a package
__version__='$Revision: 1.9 $'[11:-2]
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1997-2000 by Sam Rushing
# All Rights Reserved.
#
RCS_ID = '$Id: chat_server.py,v 1.5 2003/03/18 21:15:16 fdrake Exp $'
import string
VERSION = string.split(RCS_ID)[2]
import socket
import asyncore
import asynchat
import status_handler
class chat_channel (asynchat.async_chat):
def __init__ (self, server, sock, addr):
asynchat.async_chat.__init__ (self, sock)
self.server = server
self.addr = addr
self.set_terminator ('\r\n')
self.data = ''
self.nick = None
self.push ('nickname?: ')
def collect_incoming_data (self, data):
self.data = self.data + data
def found_terminator (self):
line = self.data
self.data = ''
if self.nick is None:
self.nick = string.split (line)[0]
if not self.nick:
self.nick = None
self.push ('huh? gimmee a nickname: ')
else:
self.greet()
else:
if not line:
pass
elif line[0] != '/':
self.server.push_line (self, line)
else:
self.handle_command (line)
def greet (self):
self.push ('Hello, %s\r\n' % self.nick)
num_channels = len(self.server.channels)-1
if num_channels == 0:
self.push ('[Kinda lonely in here... you\'re the only caller!]\r\n')
else:
self.push ('[There are %d other callers]\r\n' % (len(self.server.channels)-1))
nicks = map (lambda x: x.get_nick(), self.server.channels.keys())
self.push (string.join (nicks, '\r\n ') + '\r\n')
self.server.push_line (self, '[joined]')
def handle_command (self, command):
import types
command_line = string.split(command)
name = 'cmd_%s' % command_line[0][1:]
if hasattr (self, name):
# make sure it's a method...
method = getattr (self, name)
if type(method) == type(self.handle_command):
method (command_line[1:])
else:
self.push ('unknown command: %s' % command_line[0])
def cmd_quit (self, args):
self.server.push_line (self, '[left]')
self.push ('Goodbye!\r\n')
self.close_when_done()
# alias for '/quit' - '/q'
cmd_q = cmd_quit
def push_line (self, nick, line):
self.push ('%s: %s\r\n' % (nick, line))
def handle_close (self):
self.close()
def close (self):
del self.server.channels[self]
asynchat.async_chat.close (self)
def get_nick (self):
if self.nick is not None:
return self.nick
else:
return 'Unknown'
class chat_server (asyncore.dispatcher):
SERVER_IDENT = 'Chat Server (V%s)' % VERSION
channel_class = chat_channel
spy = 1
def __init__ (self, ip='', port=8518):
self.port = port
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.bind ((ip, port))
print '%s started on port %d' % (self.SERVER_IDENT, port)
self.listen (5)
self.channels = {}
self.count = 0
def handle_accept (self):
conn, addr = self.accept()
self.count = self.count + 1
print 'client #%d - %s:%d' % (self.count, addr[0], addr[1])
self.channels[self.channel_class (self, conn, addr)] = 1
def push_line (self, from_channel, line):
nick = from_channel.get_nick()
if self.spy:
print '%s: %s' % (nick, line)
for c in self.channels.keys():
if c is not from_channel:
c.push ('%s: %s\r\n' % (nick, line))
def status (self):
lines = [
'<h2>%s</h2>' % self.SERVER_IDENT,
'<br>Listening on Port: %d' % self.port,
'<br><b>Total Sessions:</b> %d' % self.count,
'<br><b>Current Sessions:</b> %d' % (len(self.channels))
]
return status_handler.lines_producer (lines)
def writable (self):
return 0
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
port = string.atoi (sys.argv[1])
else:
port = 8518
s = chat_server ('', port)
asyncore.loop()
# -*- Mode: Python; tab-width: 4 -*-
# It is tempting to add an __int__ method to this class, but it's not
# a good idea. This class tries to gracefully handle integer
# overflow, and to hide this detail from both the programmer and the
# user. Note that the __str__ method can be relied on for printing out
# the value of a counter:
#
# >>> print 'Total Client: %s' % self.total_clients
#
# If you need to do arithmetic with the value, then use the 'as_long'
# method, the use of long arithmetic is a reminder that the counter
# will overflow.
class counter:
"general-purpose counter"
def __init__ (self, initial_value=0):
self.value = initial_value
def increment (self, delta=1):
result = self.value
try:
self.value = self.value + delta
except OverflowError:
self.value = long(self.value) + delta
return result
def decrement (self, delta=1):
result = self.value
try:
self.value = self.value - delta
except OverflowError:
self.value = long(self.value) - delta
return result
def as_long (self):
return long(self.value)
def __nonzero__ (self):
return self.value != 0
def __repr__ (self):
return '<counter value=%s at %x>' % (self.value, id(self))
def __str__ (self):
return str(long(self.value))[:-1]
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1997 by Sam Rushing
# All Rights Reserved.
#
RCS_ID = '$Id: default_handler.py,v 1.9 2003/03/18 21:15:16 fdrake Exp $'
# standard python modules
import os
import re
import posixpath
import stat
import string
import time
# medusa modules
import http_date
import http_server
import mime_type_table
import status_handler
import producers
unquote = http_server.unquote
# This is the 'default' handler. it implements the base set of
# features expected of a simple file-delivering HTTP server. file
# services are provided through a 'filesystem' object, the very same
# one used by the FTP server.
#
# You can replace or modify this handler if you want a non-standard
# HTTP server. You can also derive your own handler classes from
# it.
#
# support for handling POST requests is available in the derived
# class <default_with_post_handler>, defined below.
#
from counter import counter
class default_handler:
valid_commands = ['get', 'head']
IDENT = 'Default HTTP Request Handler'
# Pathnames that are tried when a URI resolves to a directory name
directory_defaults = [
'index.html',
'default.html'
]
default_file_producer = producers.file_producer
def __init__ (self, filesystem):
self.filesystem = filesystem
# count total hits
self.hit_counter = counter()
# count file deliveries
self.file_counter = counter()
# count cache hits
self.cache_counter = counter()
hit_counter = 0
def __repr__ (self):
return '<%s (%s hits) at %x>' % (
self.IDENT,
self.hit_counter,
id (self)
)
# always match, since this is a default
def match (self, request):
return 1
# handle a file request, with caching.
def handle_request (self, request):
if request.command not in self.valid_commands:
request.error (400) # bad request
return
self.hit_counter.increment()
path, params, query, fragment = request.split_uri()
if '%' in path:
path = unquote (path)
# strip off all leading slashes
while path and path[0] == '/':
path = path[1:]
if self.filesystem.isdir (path):
if path and path[-1] != '/':
request['Location'] = 'http://%s/%s/' % (
request.channel.server.server_name,
path
)
request.error (301)
return
# we could also generate a directory listing here,
# may want to move this into another method for that
# purpose
found = 0
if path and path[-1] != '/':
path = path + '/'
for default in self.directory_defaults:
p = path + default
if self.filesystem.isfile (p):
path = p
found = 1
break
if not found:
request.error (404) # Not Found
return
elif not self.filesystem.isfile (path):
request.error (404) # Not Found
return
file_length = self.filesystem.stat (path)[stat.ST_SIZE]
ims = get_header_match (IF_MODIFIED_SINCE, request.header)
length_match = 1
if ims:
length = ims.group (4)
if length:
try:
length = string.atoi (length)
if length != file_length:
length_match = 0
except:
pass
ims_date = 0
if ims:
ims_date = http_date.parse_http_date (ims.group (1))
try:
mtime = self.filesystem.stat (path)[stat.ST_MTIME]
except:
request.error (404)
return
if length_match and ims_date:
if mtime <= ims_date:
request.reply_code = 304
request.done()
self.cache_counter.increment()
return
try:
file = self.filesystem.open (path, 'rb')
except IOError:
request.error (404)
return
request['Last-Modified'] = http_date.build_http_date (mtime)
request['Content-Length'] = file_length
self.set_content_type (path, request)
if request.command == 'get':
request.push (self.default_file_producer (file))
self.file_counter.increment()
request.done()
def set_content_type (self, path, request):
ext = string.lower (get_extension (path))
if mime_type_table.content_type_map.has_key (ext):
request['Content-Type'] = mime_type_table.content_type_map[ext]
else:
# TODO: test a chunk off the front of the file for 8-bit
# characters, and use application/octet-stream instead.
request['Content-Type'] = 'text/plain'
def status (self):
return producers.simple_producer (
'<li>%s' % status_handler.html_repr (self)
+ '<ul>'
+ ' <li><b>Total Hits:</b> %s' % self.hit_counter
+ ' <li><b>Files Delivered:</b> %s' % self.file_counter
+ ' <li><b>Cache Hits:</b> %s' % self.cache_counter
+ '</ul>'
)
# HTTP/1.0 doesn't say anything about the "; length=nnnn" addition
# to this header. I suppose it's purpose is to avoid the overhead
# of parsing dates...
IF_MODIFIED_SINCE = re.compile (
'If-Modified-Since: ([^;]+)((; length=([0-9]+)$)|$)',
re.IGNORECASE
)
USER_AGENT = re.compile ('User-Agent: (.*)', re.IGNORECASE)
CONTENT_TYPE = re.compile (
r'Content-Type: ([^;]+)((; boundary=([A-Za-z0-9\'\(\)+_,./:=?-]+)$)|$)',
re.IGNORECASE
)
get_header = http_server.get_header
get_header_match = http_server.get_header_match
def get_extension (path):
dirsep = string.rfind (path, '/')
dotsep = string.rfind (path, '.')
if dotsep > dirsep:
return path[dotsep+1:]
else:
return ''
<html>
<head>
<title>Licensing terms for Medusa</title>
</head>
<body>
<h1>Medusa is now Free!</h1>
<p>
Medusa was previously distributed under a 'free for
non-commercial use' license. In May of 2000 I changed the
license to be identical to the standard Python license. The
standard Python license has always applied to the core
components of Medusa, this change just frees up the rest of the
system, including the http server, ftp server, utilities, etc..
</p>
<p>
I would like to take this opportunity to thank all of the folks
who supported Medusa over the years by purchasing commercial
licenses.
</p>
</body>
</html>
<html>
<body>
Medusa is Copyright 1996-1997, Sam Rushing (rushing@nightmare.com)
<hr>
<pre>
Medusa is provided free for all non-commercial use. If you are using
Medusa to make money, or you would like to distribute Medusa or any
derivative of Medusa commercially, then you must arrange a license
with me. Extension authors may either negotiate with me to include
their extension in the main distribution, or may distribute under
their own terms.
You may modify or extend Medusa, but you may not redistribute the
modified versions without permission.
<b>
NIGHTMARE SOFTWARE AND SAM RUSHING DISCLAIM ALL WARRANTIES WITH REGARD
TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS, IN NO EVENT SHALL NIGHTMARE SOFTWARE OR 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.
</b>
</pre>
For more information please contact me at <a href="mailto:rushing@nightmare.com">
rushing@nightmare.com</a>
<h1> What is Medusa? </h1>
<hr>
<p>
Medusa is an architecture for very-high-performance TCP/IP servers
(like HTTP, FTP, and NNTP). Medusa is different from most other
servers because it runs as a single process, multiplexing I/O with its
various client and server connections within a single process/thread.
<p>
It is capable of smoother and higher performance than most other
servers, while placing a dramatically reduced load on the server
machine. The single-process, single-thread model simplifies design
and enables some new persistence capabilities that are otherwise
difficult or impossible to implement.
<p>
Medusa is supported on any platform that can run Python and includes a
functional implementation of the &lt;socket&gt; and &lt;select&gt;
modules. This includes the majority of Unix implementations.
<p>
During development, it is constantly tested on Linux and Win32
[Win95/WinNT], but the core asynchronous capability has been shown to
work on several other platforms, including the Macintosh. It might
even work on VMS.
<h2>The Power of Python</h2>
<p>
A distinguishing feature of Medusa is that it is written entirely in
Python. Python (<a href="http://www.python.org/">http://www.python.org/</a>) is a
'very-high-level' object-oriented language developed by Guido van
Rossum (currently at CNRI). It is easy to learn, and includes many
modern programming features such as storage management, dynamic
typing, and an extremely flexible object system. It also provides
convenient interfaces to C and C++.
<p>
The rapid prototyping and delivery capabilities are hard to exaggerate;
for example
<ul>
<li>It took me longer to read the documentation for persistent HTTP
connections (the 'Keep-Alive' connection token) than to add the
feature to Medusa.
<li>A simple IRC-like chat server system was written in about 90 minutes.
</ul>
<p> I've heard similar stories from alpha test sites, and other users of
the core async library.
<h2>Server Notes</h2>
<p>Both the FTP and HTTP servers use an abstracted 'filesystem object' to
gain access to a given directory tree. One possible server extension
technique would be to build behavior into this filesystem object,
rather than directly into the server: Then the extension could be
shared with both the FTP and HTTP servers.
<h3>HTTP</h3>
<p>The core HTTP server itself is quite simple - all functionality is
provided through 'extensions'. Extensions can be plugged in
dynamically. [i.e., you could log in to the server via the monitor
service and add or remove an extension on the fly]. The basic
file-delivery service is provided by a 'default' extension, which
matches all URI's. You can build more complex behavior by replacing
or extending this class.
<p>The default extension includes support for the 'Connection: Keep-Alive'
token, and will re-use a client channel when requested by the client.
<h3>FTP</h3>
<p>On Unix, the ftp server includes support for 'real' users, so that it
may be used as a drop-in replacement for the normal ftp server. Since
most ftp servers on Unix use the 'forking' model, each child process
changes its user/group persona after a successful login. This is a
appears to be a secure design.
<p>Medusa takes a different approach - whenever Medusa performs an
operation for a particular user [listing a directory, opening a file],
it temporarily switches to that user's persona _only_ for the duration
of the operation. [and each such operation is protected by a
try/finally exception handler].
<p>To do this Medusa MUST run with super-user privileges. This is a
HIGHLY experimental approach, and although it has been thoroughly
tested on Linux, security problems may still exist. If you are
concerned about the security of your server machine, AND YOU SHOULD
BE, I suggest running Medusa's ftp server in anonymous-only mode,
under an account with limited privileges ('nobody' is usually used for
this purpose).
<p>I am very interested in any feedback on this feature, most
especially information on how the server behaves on different
implementations of Unix, and of course any security problems that are
found.
<hr>
<h3>Monitor</h3>
<p>The monitor server gives you remote, 'back-door' access to your server
while it is running. It implements a remote python interpreter. Once
connected to the monitor, you can do just about anything you can do from
the normal python interpreter. You can examine data structures, servers,
connection objects. You can enable or disable extensions, restart the server,
reload modules, etc...
<p>The monitor server is protected with an MD5-based authentication
similar to that proposed in RFC1725 for the POP3 protocol. The server
sends the client a timestamp, which is then appended to a secret
password. The resulting md5 digest is sent back to the server, which
then compares this to the expected result. Failed login attempts are
logged and immediately disconnected. The password itself is not sent
over the network (unless you have foolishly transmitted it yourself
through an insecure telnet or X11 session. 8^)
<p>For this reason telnet cannot be used to connect to the monitor
server when it is in a secure mode (the default). A client program is
provided for this purpose. You will be prompted for a password when
starting up the server, and by the monitor client.
<p>For extra added security on Unix, the monitor server will
eventually be able to use a Unix-domain socket, which can be protected
behind a 'firewall' directory (similar to the InterNet News server).
<hr>
<h2>Performance Notes</h2>
<h3>The <code>select()</code> function</h3>
<p>At the heart of Medusa is a single <code>select()</code> loop.
This loop handles all open socket connections, both servers and
clients. It is in effect constantly asking the system: 'which of
these sockets has activity?'. Performance of this system call can
vary widely between operating systems.
<p>There are also often builtin limitations to the number of sockets
('file descriptors') that a single process, or a whole system, can
manipulate at the same time. Early versions of Linux placed draconian
limits (256) that have since been raised. Windows 95 has a limit of
64, while OSF/1 seems to allow up to 4096.
<p>These limits don't affect only Medusa, you will find them described
in the documentation for other web and ftp servers, too.
<p>The documentation for the Apache web server has some excellent
notes on tweaking performance for various Unix implementations. See
<a href="http://www.apache.org/docs/misc/perf.html">
http://www.apache.org/docs/misc/perf.html</a>
for more information.
<h3>Buffer sizes</h3>
<p>
The default buffer sizes used by Medusa are set with a bias toward
Internet-based servers: They are relatively small, so that the buffer
overhead for each connection is low. The assumption is that Medusa
will be talking to a large number of low-bandwidth connections, rather
than a smaller number of high bandwidth.
<p>This choice trades run-time memory use for efficiency - the down
side of this is that high-speed local connections (i.e., over a local
ethernet) will transfer data at a slower rate than necessary.
<p>This parameter can easily be tweaked by the site designer, and can
in fact be adjusted on a per-server or even per-client basis. For
example, you could have the FTP server use larger buffer sizes for
connections from certain domains.
<p>If there's enough interest, I have some rough ideas for how to make
these buffer sizes automatically adjust to an optimal setting. Send
email if you'd like to see this feature.
<hr>
<p>See <a href="medusa.html">./medusa.html</a> for a brief overview of
some of the ideas behind Medusa's design, and for a description of
current and upcoming features.
<p><h3>Enjoy!</h3>
<hr>
<br>-Sam Rushing
<br><a href="mailto:rushing@nightmare.com">rushing@nightmare.com</a>
<!--
Local Variables:
indent-use-tabs: nil
end:
-->
</body>
</html>
<h1>Data Flow in Medusa</h1>
<img src="data_flow.gif">
<p>Data flow, both input and output, is asynchronous. This is
signified by the <i>request</i> and <i>reply</i> queues in the above
diagram. This means that both requests and replies can get 'backed
up', and are still handled correctly. For instance, HTTP/1.1 supports
the concept of <i>pipelined requests</i>, where a series of requests
are sent immediately to a server, and the replies are sent as they are
processed. With a <i>synchronous</i> request, the client would have
to wait for a reply to each request before sending the next.</p>
<p>The input data is partitioned into requests by looking for a
<i>terminator</i>. A terminator is simply a protocol-specific
delimiter - often simply CRLF (carriage-return line-feed), though it
can be longer (for example, MIME multi-part boundaries can be
specified as terminators). The protocol handler is notified whenever
a complete request has been received.</p>
<p>The protocol handler then generates a reply, which is enqueued for
output back to the client. Sometimes, instead of queuing the actual
data, an object that will generate this data is used, called a
<i>producer</i>.</p>
<img src="producers.gif">
<p>The use of <code>producers</code> gives the programmer
extraordinary control over how output is generated and inserted into
the output queue. Though they are simple objects (requiring only a
single method, <i>more()</i>, to be defined), they can be
<i>composed</i> - simple producers can be wrapped around each other to
create arbitrarily complex behaviors. [now would be a good time to
browse through some of the producer classes in
<code>producers.py</code>.]</p>
<p>The HTTP/1.1 producers make an excellent example. HTTP allows
replies to be encoded in various ways - for example a reply consisting
of dynamically-generated output might use the 'chunked' transfer
encoding to send data that is compressed on-the-fly.</p>
<img src="composing_producers.gif">
<p>In the diagram, green producers actually generate output, and grey
ones transform it in some manner. This producer might generate output
looking like this:
<pre>
HTTP/1.1 200 OK
Content-Encoding: gzip
Transfer-Encoding: chunked
Header ==> Date: Mon, 04 Aug 1997 21:31:44 GMT
Content-Type: text/html
Server: Medusa/3.0
Chunking ==> 0x200
Compression ==> <512 bytes of compressed html>
0x200
<512 bytes of compressed html>
...
0
</pre>
<p>Still more can be done with this output stream: For the purpose of
efficiency, it makes sense to send output in large, fixed-size chunks:
This transformation can be applied by wrapping a 'globbing' producer
around the whole thing.</p>
<p>An important feature of Medusa's producers is that they are
actually rather small objects that do not expand into actual output
data until the moment they are needed: The <code>async_chat</code>
class will only call on a producer for output when the outgoing socket
has indicated that it is ready for data. Thus Medusa is extremely
efficient when faced with network delays, 'hiccups', and low bandwidth
clients.
<p>One final note: The mechanisms described above are completely
general - although the examples given demonstrate application to the
<code>http</code> protocol, Medusa's asynchronous core has been
applied to many different protocols, including <code>smtp</code>,
<code>pop3</code>, <code>ftp</code>, and even <code>dns</code>.
# we can build 'promises' to produce external data. Each producer
# contains a 'promise' to fetch external data (or an error
# message). writable() for that channel will only return true if the
# top-most producer is ready. This state can be flagged by the dns
# client making a callback.
# So, say 5 proxy requests come in, we can send out DNS queries for
# them immediately. If the replies to these come back before the
# promises get to the front of the queue, so much the better: no
# resolve delay. 8^)
#
# ok, there's still another complication:
# how to maintain replies in order?
# say three requests come in, (to different hosts? can this happen?)
# yet the connections happen third, second, and first. We can't buffer
# the entire request! We need to be able to specify how much to buffer.
#
# ===========================================================================
#
# the current setup is a 'pull' model: whenever the channel fires FD_WRITE,
# we 'pull' data from the producer fifo. what we need is a 'push' option/mode,
# where
# 1) we only check for FD_WRITE when data is in the buffer
# 2) whoever is 'pushing' is responsible for calling 'refill_buffer()'
#
# what is necessary to support this 'mode'?
# 1) writable() only fires when data is in the buffer
# 2) refill_buffer() is only called by the 'pusher'.
#
# how would such a mode affect things? with this mode could we support
# a true http/1.1 proxy? [i.e, support <n> pipelined proxy requests, possibly
# to different hosts, possibly even mixed in with non-proxy requests?] For
# example, it would be nice if we could have the proxy automatically apply the
# 1.1 chunking for 1.0 close-on-eof replies when feeding it to the client. This
# would let us keep our persistent connection.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# -*- Mode: Python -*-
# the medusa icon as a python source file.
width = 97
height = 61
data = 'GIF89aa\000=\000\204\000\000\000\000\000\255\255\255\245\245\245ssskkkccc111)))\326\326\326!!!\316\316\316\300\300\300\204\204\000\224\224\224\214\214\214\200\200\200RRR\377\377\377JJJ\367\367\367BBB\347\347\347\000\204\000\020\020\020\265\265\265\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000!\371\004\001\000\000\021\000,\000\000\000\000a\000=\000\000\005\376`$\216di\236h\252\256l\353\276p,\317tm\337x\256\357|m\001@\240E\305\000\364\2164\206R)$\005\201\214\007r\012{X\255\312a\004\260\\>\026\3240\353)\224n\001W+X\334\373\231~\344.\303b\216\024\027x<\273\307\255G,rJiWN\014{S}k"?ti\013EdPQ\207G@_%\000\026yy\\\201\202\227\224<\221Fs$pOjWz\241<r@vO\236\231\233k\247M\2544\203F\177\235\236L#\247\256Z\270,\266BxJ[\276\256A]iE\304\305\262\273E\313\201\275i#\\\303\321\'h\203V\\\177\326\276\216\220P~\335\230_\264\013\342\275\344KF\233\360Q\212\352\246\000\367\274s\361\236\334\347T\341;\341\246\2202\177\3142\211`\242o\325@S\202\264\031\252\207\260\323\256\205\311\036\236\270\002\'\013\302\177\274H\010\324X\002\0176\212\037\376\321\360\032\226\207\244\2674(+^\202\346r\205J\0211\375\241Y#\256f\0127\315>\272\002\325\307g\012(\007\205\312#j\317(\012A\200\224.\241\003\346GS\247\033\245\344\264\366\015L\'PXQl]\266\263\243\232\260?\245\316\371\362\225\035\332\243J\273\332Q\263\357-D\241T\327\270\265\013W&\330\010u\371b\322IW0\214\261]\003\033Va\365Z#\207\213a\030k\2647\262\014p\354\024[n\321N\363\346\317\003\037P\000\235C\302\000\3228(\244\363YaA\005\022\255_\237@\260\000A\212\326\256qbp\321\332\266\011\334=T\023\010"!B\005\003A\010\224\020\220 H\002\337#\020 O\276E\357h\221\327\003\\\000b@v\004\351A.h\365\354\342B\002\011\257\025\\ \220\340\301\353\006\000\024\214\200pA\300\353\012\364\241k/\340\033C\202\003\000\310fZ\011\003V\240R\005\007\354\376\026A\000\000\360\'\202\177\024\004\210\003\000\305\215\360\000\000\015\220\240\332\203\027@\'\202\004\025VpA\000%\210x\321\206\032J\341\316\010\262\211H"l\333\341\200\200>"]P\002\212\011\010`\002\0066FP\200\001\'\024p]\004\027(8B\221\306]\000\201w>\002iB\001\007\340\260"v7J1\343(\257\020\251\243\011\242i\263\017\215\337\035\220\200\221\365m4d\015\016D\251\341iN\354\346Ng\253\200I\240\031\35609\245\2057\311I\302\2007t\231"&`\314\310\244\011e\226(\236\010w\212\300\234\011\012HX(\214\253\311@\001\233^\222pg{% \340\035\224&H\000\246\201\362\215`@\001"L\340\004\030\234\022\250\'\015(V:\302\235\030\240q\337\205\224\212h@\177\006\000\250\210\004\007\310\207\337\005\257-P\346\257\367]p\353\203\271\256:\203\236\211F\340\247\010\3329g\244\010\307*=A\000\203\260y\012\304s#\014\007D\207,N\007\304\265\027\021C\233\207%B\366[m\353\006\006\034j\360\306+\357\274a\204\000\000;'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# make test appear as a package
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# make thread to appear as a package
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment