SVN: r25476 - in trunk/quixote: . demo server
Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 27 Oct 2004 17:56:10 -0400
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: nascheme
Date: 2004-10-27 17:54:59 -0400 (Wed, 27 Oct 2004)
New Revision: 25476
Added:
trunk/quixote/server/_fcgi.py
trunk/quixote/server/cgi_server.py
trunk/quixote/server/fastcgi_server.py
trunk/quixote/server/medusa_server.py
trunk/quixote/server/scgi_server.py
trunk/quixote/server/twisted_server.py
Removed:
trunk/quixote/demo/demo.cgi
trunk/quixote/fcgi.py
trunk/quixote/server/medusa_http.py
trunk/quixote/server/twisted_http.py
Modified:
trunk/quixote/demo/__init__.py
trunk/quixote/demo/integer_ui.py
trunk/quixote/demo/pages.ptl
trunk/quixote/http_response.py
trunk/quixote/publish.py
trunk/quixote/server/__init__.py
Log:
Use a common pattern for interfacing with HTTP servers. Simplify
and cleanup the Medusa and Twisted code. Add a SCGI server module
(it makes more sense to be part of Quixote rather than in the "scgi"
package).
The main method of the Publisher is now process_request(). It takes
a request object and returns a response object. All the server modules
use it and it's their job to create requests and deal with the
responses.
Add a 'content_type' attribute to HTTPResponse. Refactor to make it
harder to screw up the character set of the response.
Modified: trunk/quixote/demo/__init__.py
===================================================================
--- trunk/quixote/demo/__init__.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/demo/__init__.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -5,14 +5,16 @@
import sys
import os
-from quixote import get_response
+from quixote import get_response, enable_ptl
+from quixote.publish import Publisher
from quixote.directory import Directory, Resolving
+from quixote.errors import PublishError
+from quixote.util import StaticDirectory, StaticFile
+enable_ptl()
from quixote.demo.pages import _q_index, _q_exception_handler, dumpreq
from quixote.demo.integer_ui import IntegerUI
from quixote.demo.session import SessionUI
from quixote.demo import forms
-from quixote.errors import PublishError
-from quixote.util import StaticDirectory, StaticFile
class DemoUI(Resolving, Directory):
@@ -52,3 +54,7 @@
_curdir = os.path.dirname(forms.__file__)
srcdir = StaticDirectory(_curdir, list_directory=1)
q_ico = StaticFile(os.path.join(_curdir, 'q.ico'))
+
+
+def create_publisher():
+ return Publisher(DemoUI(), display_exceptions='plain')
Deleted: trunk/quixote/demo/demo.cgi
===================================================================
--- trunk/quixote/demo/demo.cgi 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/demo/demo.cgi 2004-10-27 21:54:59 UTC (rev 25476)
@@ -1,16 +0,0 @@
-#!/www/python/bin/python
-
-# Example driver script for the Quixote demo: publishes the contents of
-# the quixote.demo package.
-
-from quixote import enable_ptl, Publisher
-
-# Install the import hook that enables PTL modules.
-enable_ptl()
-
-# Create a Publisher instance
-from quixote.demo import DemoUI
-app = Publisher(DemoUI(), display_exceptions='plain')
-
-# Enter the publishing main loop
-app.publish_cgi()
Modified: trunk/quixote/demo/integer_ui.py
===================================================================
--- trunk/quixote/demo/integer_ui.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/demo/integer_ui.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -24,7 +24,7 @@
if self.n > 10000:
sys.stderr.write("warning: possible denial-of-service attack "
"(request for factorial(%d))\n" % self.n)
- get_response().set_header("content-type", "text/plain")
+ get_response().set_content_type("text/plain")
return "%d! = %d\n" % (self.n, fact(self.n))
def _q_index(self):
Modified: trunk/quixote/demo/pages.ptl
===================================================================
--- trunk/quixote/demo/pages.ptl 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/demo/pages.ptl 2004-10-27 21:54:59 UTC (rev 25476)
@@ -11,7 +11,6 @@
print "debug message from the index page"
package_name = str('.').join(__name__.split(str('.'))[:-1])
module_name = __name__
- module_file = __file__
"""
<html>
<head><title>Quixote Demo</title></head>
@@ -22,7 +21,7 @@
<code>%(package_name)s</code> package. This index function is
actually a PTL template, <code>_q_index()</code>, in the
<code>%(module_name)s</code> PTL module. Look in
- <a href="srcdir/pages.ptl">%(module_file)s</a> to
+ <a href="srcdir/pages.ptl">demo/pages.ptl</a> to
see the source code for this PTL template.)
</p>
Deleted: trunk/quixote/fcgi.py
===================================================================
--- trunk/quixote/fcgi.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/fcgi.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -1,462 +0,0 @@
-#!/usr/local/bin/python1.5
-#------------------------------------------------------------------------
-# Copyright (c) 1998 by Total Control Software
-# All Rights Reserved
-#------------------------------------------------------------------------
-#
-# Module Name: fcgi.py
-#
-# Description: Handles communication with the FastCGI module of the
-# web server without using the FastCGI developers kit, but
-# will also work in a non-FastCGI environment, (straight CGI.)
-# This module was originally fetched from someplace on the
-# Net (I don't remember where and I can't find it now...) and
-# has been significantly modified to fix several bugs, be more
-# readable, more robust at handling large CGI data and return
-# document sizes, and also to fit the model that we had previously
-# used for FastCGI.
-#
-# WARNING: If you don't know what you are doing, don't tinker with this
-# module!
-#
-# Creation Date: 1/30/98 2:59:04PM
-#
-# License: This is free software. You may use this software for any
-# purpose including modification/redistribution, so long as
-# this header remains intact and that you do not claim any
-# rights of ownership or authorship of this software. This
-# software has been tested, but no warranty is expressed or
-# implied.
-#
-#------------------------------------------------------------------------
-
-__revision__ = "$Id$"
-
-
-import os, sys, string, socket, errno, struct
-from cStringIO import StringIO
-import cgi
-
-#---------------------------------------------------------------------------
-
-# Set various FastCGI constants
-# Maximum number of requests that can be handled
-FCGI_MAX_REQS=1
-FCGI_MAX_CONNS = 1
-
-# Supported version of the FastCGI protocol
-FCGI_VERSION_1 = 1
-
-# Boolean: can this application multiplex connections?
-FCGI_MPXS_CONNS=0
-
-# Record types
-FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3
-FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6
-FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9
-FCGI_GET_VALUES_RESULT = 10
-FCGI_UNKNOWN_TYPE = 11
-FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
-
-# Types of management records
-ManagementTypes = [FCGI_GET_VALUES]
-
-FCGI_NULL_REQUEST_ID = 0
-
-# Masks for flags component of FCGI_BEGIN_REQUEST
-FCGI_KEEP_CONN = 1
-
-# Values for role component of FCGI_BEGIN_REQUEST
-FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3
-
-# Values for protocolStatus component of FCGI_END_REQUEST
-FCGI_REQUEST_COMPLETE = 0 # Request completed nicely
-FCGI_CANT_MPX_CONN = 1 # This app can't multiplex
-FCGI_OVERLOADED = 2 # New request rejected; too busy
-FCGI_UNKNOWN_ROLE = 3 # Role value not known
-
-
-error = 'fcgi.error'
-
-
-#---------------------------------------------------------------------------
-
-# The following function is used during debugging; it isn't called
-# anywhere at the moment
-
-def error(msg):
- "Append a string to /tmp/err"
- errf = open('/tmp/err', 'a+')
- errf.write(msg+'\n')
- errf.close()
-
-#---------------------------------------------------------------------------
-
-class record:
- "Class representing FastCGI records"
- def __init__(self):
- self.version = FCGI_VERSION_1
- self.recType = FCGI_UNKNOWN_TYPE
- self.reqId = FCGI_NULL_REQUEST_ID
- self.content = ""
-
- #----------------------------------------
- def readRecord(self, sock, unpack=struct.unpack):
- (self.version, self.recType, self.reqId, contentLength,
- paddingLength) = unpack(">BBHHBx", sock.recv(8))
-
- content = ""
- while len(content) < contentLength:
- content = content + sock.recv(contentLength - len(content))
- self.content = content
-
- if paddingLength != 0:
- padding = sock.recv(paddingLength)
-
- # Parse the content information
- if self.recType == FCGI_BEGIN_REQUEST:
- (self.role, self.flags) = unpack(">HB", content[:3])
-
- elif self.recType == FCGI_UNKNOWN_TYPE:
- self.unknownType = ord(content[0])
-
- elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
- self.values = {}
- pos = 0
- while pos < len(content):
- name, value, pos = readPair(content, pos)
- self.values[name] = value
-
- elif self.recType == FCGI_END_REQUEST:
- (self.appStatus, self.protocolStatus) = unpack(">IB", content[0:5])
-
- #----------------------------------------
- def writeRecord(self, sock, pack=struct.pack):
- content = self.content
- if self.recType == FCGI_BEGIN_REQUEST:
- content = pack(">HBxxxxx", self.role, self.flags)
-
- elif self.recType == FCGI_UNKNOWN_TYPE:
- content = pack(">Bxxxxxx", self.unknownType)
-
- elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
- content = ""
- for i in self.values.keys():
- content = content + writePair(i, self.values[i])
-
- elif self.recType == FCGI_END_REQUEST:
- content = pack(">IBxxx", self.appStatus, self.protocolStatus)
-
- cLen = len(content)
- eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary
- padLen = eLen - cLen
-
- hdr = pack(">BBHHBx", self.version, self.recType, self.reqId, cLen,
- padLen)
-
- ##debug.write('Sending fcgi record: %s\n' % repr(content[:50]) )
- sock.send(hdr + content + padLen*'\000')
-
-#---------------------------------------------------------------------------
-
-_lowbits = ~(1L << 31) # everything but the 31st bit
-
-def readPair(s, pos):
- nameLen = ord(s[pos]) ; pos = pos+1
- if nameLen & 128:
- pos = pos + 3
- nameLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
- valueLen = ord(s[pos]) ; pos = pos+1
- if valueLen & 128:
- pos = pos + 3
- valueLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
- return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen],
- pos+nameLen+valueLen )
-
-#---------------------------------------------------------------------------
-
-_highbit = (1L << 31)
-
-def writePair(name, value):
- l = len(name)
- if l < 128:
- s = chr(l)
- else:
- s = struct.pack(">I", l | _highbit)
- l = len(value)
- if l < 128:
- s = s + chr(l)
- else:
- s = s + struct.pack(">I", l | _highbit)
- return s + name + value
-
-#---------------------------------------------------------------------------
-
-def HandleManTypes(r, conn):
- if r.recType == FCGI_GET_VALUES:
- r.recType = FCGI_GET_VALUES_RESULT
- v = {}
- vars = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS,
- 'FCGI_MAX_REQS' : FCGI_MAX_REQS,
- 'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS}
- for i in r.values.keys():
- if vars.has_key(i): v[i] = vars[i]
- r.values = vars
- r.writeRecord(conn)
-
-#---------------------------------------------------------------------------
-#---------------------------------------------------------------------------
-
-
-_isFCGI = 1 # assume it is until we find out for sure
-
-def isFCGI():
- return _isFCGI
-
-
-
-#---------------------------------------------------------------------------
-
-
-_init = None
-_sock = None
-
-class FCGI:
- def __init__(self):
- self.haveFinished = 0
- if _init == None:
- _startup()
- if not _isFCGI:
- self.haveFinished = 1
- self.inp = sys.__stdin__
- self.out = sys.__stdout__
- self.err = sys.__stderr__
- self.env = os.environ
- return
-
- if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'):
- good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',')
- good_addrs = map(string.strip, good_addrs) # Remove whitespace
- else:
- good_addrs = None
-
- self.conn, addr = _sock.accept()
- stdin, data = "", ""
- self.env = {}
- self.requestId = 0
- remaining = 1
-
- # Check if the connection is from a legal address
- if good_addrs != None and addr not in good_addrs:
- raise error, 'Connection from invalid server!'
-
- while remaining:
- r = record()
- r.readRecord(self.conn)
-
- if r.recType in ManagementTypes:
- HandleManTypes(r, self.conn)
-
- elif r.reqId == 0:
- # Oh, poopy. It's a management record of an unknown
- # type. Signal the error.
- r2 = record()
- r2.recType = FCGI_UNKNOWN_TYPE
- r2.unknownType = r.recType
- r2.writeRecord(self.conn)
- continue # Charge onwards
-
- # Ignore requests that aren't active
- elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST:
- continue
-
- # If we're already doing a request, ignore further BEGIN_REQUESTs
- elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0:
- continue
-
- # Begin a new request
- if r.recType == FCGI_BEGIN_REQUEST:
- self.requestId = r.reqId
- if r.role == FCGI_AUTHORIZER: remaining = 1
- elif r.role == FCGI_RESPONDER: remaining = 2
- elif r.role == FCGI_FILTER: remaining = 3
-
- elif r.recType == FCGI_PARAMS:
- if r.content == "":
- remaining = remaining-1
- else:
- for i in r.values.keys():
- self.env[i] = r.values[i]
-
- elif r.recType == FCGI_STDIN:
- if r.content == "":
- remaining = remaining-1
- else:
- stdin = stdin+r.content
-
- elif r.recType == FCGI_DATA:
- if r.content == "":
- remaining = remaining-1
- else:
- data = data+r.content
- # end of while remaining:
-
- self.inp = StringIO(stdin)
- self.err = StringIO()
- self.out = StringIO()
- self.data = StringIO(data)
-
- def __del__(self):
- self.Finish()
-
- def Finish(self, status=0):
- if not self.haveFinished:
- self.haveFinished = 1
-
- self.err.seek(0,0)
- self.out.seek(0,0)
-
- ##global debug
- ##debug = open("/tmp/quixote-debug.log", "a+")
- ##debug.write("fcgi.FCGI.Finish():\n")
-
- r = record()
- r.recType = FCGI_STDERR
- r.reqId = self.requestId
- data = self.err.read()
- ##debug.write(" sending stderr (%s)\n" % `self.err`)
- ##debug.write(" data = %s\n" % `data`)
- while data:
- chunk, data = self.getNextChunk(data)
- ##debug.write(" chunk, data = %s, %s\n" % (`chunk`, `data`))
- r.content = chunk
- r.writeRecord(self.conn)
- r.content = ""
- r.writeRecord(self.conn) # Terminate stream
-
- r.recType = FCGI_STDOUT
- data = self.out.read()
- ##debug.write(" sending stdout (%s)\n" % `self.out`)
- ##debug.write(" data = %s\n" % `data`)
- while data:
- chunk, data = self.getNextChunk(data)
- r.content = chunk
- r.writeRecord(self.conn)
- r.content = ""
- r.writeRecord(self.conn) # Terminate stream
-
- r = record()
- r.recType = FCGI_END_REQUEST
- r.reqId = self.requestId
- r.appStatus = status
- r.protocolStatus = FCGI_REQUEST_COMPLETE
- r.writeRecord(self.conn)
- self.conn.close()
-
- #debug.close()
-
-
- def getFieldStorage(self):
- method = 'GET'
- if self.env.has_key('REQUEST_METHOD'):
- method = string.upper(self.env['REQUEST_METHOD'])
- if method == 'GET':
- return cgi.FieldStorage(environ=self.env, keep_blank_values=1)
- else:
- return cgi.FieldStorage(fp=self.inp,
- environ=self.env,
- keep_blank_values=1)
-
- def getNextChunk(self, data):
- chunk = data[:8192]
- data = data[8192:]
- return chunk, data
-
-
-Accept = FCGI # alias for backwards compatibility
-#---------------------------------------------------------------------------
-
-def _startup():
- global _isFCGI, _init, _sock
- # This function won't work on Windows at all.
- if sys.platform[:3] == 'win':
- _isFCGI = 0
- return
-
- _init = 1
- try:
- s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET,
- socket.SOCK_STREAM)
- s.getpeername()
- except socket.error, (err, errmsg):
- if err != errno.ENOTCONN: # must be a non-fastCGI environment
- _isFCGI = 0
- return
-
- _sock = s
-
-
-#---------------------------------------------------------------------------
-
-def _test():
- counter = 0
- try:
- while isFCGI():
- req = Accept()
- counter = counter+1
-
- try:
- fs = req.getFieldStorage()
- size = string.atoi(fs['size'].value)
- doc = ['*' * size]
- except:
- doc = ['<HTML><HEAD>'
- '<TITLE>FCGI TestApp</TITLE>'
- '</HEAD>\n<BODY>\n']
- doc.append('<H2>FCGI TestApp</H2><P>')
- doc.append('<b>request count</b> = %d<br>' % counter)
- doc.append('<b>pid</b> = %s<br>' % os.getpid())
- if req.env.has_key('CONTENT_LENGTH'):
- cl = string.atoi(req.env['CONTENT_LENGTH'])
- doc.append('<br><b>POST data (%s):</b><br><pre>' % cl)
- keys = fs.keys()
- keys.sort()
- for k in keys:
- val = fs[k]
- if type(val) == type([]):
- doc.append(' <b>%-15s :</b> %s\n'
- % (k, val))
- else:
- doc.append(' <b>%-15s :</b> %s\n'
- % (k, val.value))
- doc.append('</pre>')
-
-
- doc.append('<P><HR><P><pre>')
- keys = req.env.keys()
- keys.sort()
- for k in keys:
- doc.append('<b>%-20s :</b> %s\n' % (k, req.env[k]))
- doc.append('\n</pre><P><HR>\n')
- doc.append('</BODY></HTML>\n')
-
-
- doc = string.join(doc, '')
- req.out.write('Content-length: %s\r\n'
- 'Content-type: text/html\r\n'
- 'Cache-Control: no-cache\r\n'
- '\r\n'
- % len(doc))
- req.out.write(doc)
-
- req.Finish()
- except:
- import traceback
- f = open('traceback', 'w')
- traceback.print_exc( file = f )
-# f.write('%s' % doc)
-
-if __name__ == '__main__':
- #import pdb
- #pdb.run('_test()')
- _test()
Modified: trunk/quixote/http_response.py
===================================================================
--- trunk/quixote/http_response.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/http_response.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -7,6 +7,7 @@
import time
from rfc822 import formatdate
+from quixote.html import stringify
status_reasons = {
100: 'Continue',
@@ -68,8 +69,11 @@
after all).
Instance attributes:
+ content_type : string
+ the MIME content type of the response (does not include extra params
+ like charset)
charset : string
- the default character encoding of the the response
+ the character encoding of the the response
status_code : int
HTTP response status code (integer between 100 and 599)
reason_phrase : string
@@ -80,12 +84,9 @@
by 'set_header()' goes here. Does not include "Status" or
"Set-Cookie" headers (unless someone uses set_header() to set
them, but that would be foolish).
- body : string
- the response body, None by default. If the body is never
- set (ie. left as None), the response will not include
- "Content-type" or "Content-length" headers. These headers
- are set as soon as the body is set (with set_body()), even
- if the body is an empty string.
+ body : str | Stream
+ the response body, None by default. Note that if the body is not a
+ stream then it is already encoded using 'charset'.
buffered : bool
if false, response data will be flushed as soon as it is
written (the default is true). This is most useful for
@@ -113,6 +114,7 @@
"""
Creates a new HTTP response.
"""
+ self.content_type = 'text/html'
self.charset = charset
self.set_status(status)
self.headers = {}
@@ -127,6 +129,15 @@
self.buffered = True
self.javascript_code = None
+ def set_content_type(self, content_type, charset='iso-8859-1'):
+ """(content_type : string, charset : string = 'iso-8859-1')
+
+ Set the content type of the response to the MIME type specified by
+ 'content_type'. Also sets the charset, defaulting to 'iso-8859-1'.
+ """
+ self.charset = charset
+ self.content_type = content_type
+
def set_charset(self, charset):
self.charset = charset.lower()
@@ -180,35 +191,23 @@
else:
self.cache = seconds + 60*(minutes + 60*(hours + 24*days))
- def set_content_type(self, ctype):
- """set_content_type(ctype : string)
-
- Set the "Content-type" header to the MIME type specified in ctype.
- Shortcut for set_header("Content-type", ctype).
+ def _encode_chunk(self, chunk):
+ """(chunk : str | unicode) -> str
"""
- self.headers["content-type"] = ctype
-
+ if self.charset == 'iso-8859-1' and isinstance(chunk, str):
+ return chunk # non-ASCII chars are okay
+ else:
+ return chunk.encode(self.charset)
+
def set_body(self, body):
- """set_body(body : any)
+ """(body : any)
- Sets the return body equal to the argument "body". Also updates the
- "Content-length" header if the length is of the body is known. If
- the "Content-type" header has not yet been set, it is set to
- "text/html".
+ Sets the response body equal to the argument "body".
"""
if isinstance(body, Stream):
self.body = body
- if body.length is not None:
- self.set_header('content-length', body.length)
else:
- if self.charset == 'iso-8859-1':
- self.body = str(body)
- else:
- self.body = unicode(body).encode(self.charset)
- self.set_header('content-length', len(self.body))
- if not self.headers.has_key('content-type'):
- self.set_header('content-type',
- 'text/html; charset=%s' % self.charset)
+ self.body = self._encode_chunk(stringify(body))
def expire_cookie(self, name, **attrs):
"""
@@ -282,6 +281,14 @@
self.set_content_type('text/plain')
return "Your browser should have redirected you to %s" % location
+ def get_content_length(self):
+ if self.body is None:
+ return None
+ elif isinstance(self.body, Stream):
+ return self.body.length
+ else:
+ return len(self.body)
+
def _gen_cookie_headers(self):
"""_gen_cookie_headers() -> [string]
@@ -326,13 +333,13 @@
# Date header
now = time.time()
- if not self.headers.has_key("date"):
+ if "date" not in self.headers:
headers.append(("Date", formatdate(now)))
# Cache directives
if self.cache is None:
pass # don't mess with the expires header
- elif not self.headers.has_key("expires"):
+ elif "expires" not in self.headers:
if self.cache > 0:
expire_date = formatdate(now + self.cache)
else:
@@ -340,13 +347,35 @@
# with some clients
headers.append(("Expires", expire_date))
+ # Content-type
+ if "content-type" not in self.headers:
+ headers.append(('Content-Type',
+ '%s; charset=%s' % (self.content_type,
+ self.charset)))
+
+ # Content-Length
+ if "content-length" not in self.headers:
+ length = self.get_content_length()
+ if length is not None:
+ headers.append(('Content-Length', length))
+
return headers
+ def generate_body_chunks(self):
+ """Return a sequence of body chunks, encoded using 'charset'.
+ """
+ if self.body is None:
+ pass
+ elif isinstance(self.body, Stream):
+ for chunk in self.body:
+ yield self._encode_chunk(chunk)
+ else:
+ yield self.body # already encoded
- def write(self, file):
- """write(file : file)
+ def write(self, output):
+ """write(output : file)
- Write the HTTP response headers and body to 'file'. This is not
+ Write the HTTP response headers and body to 'output'. This is not
a complete HTTP response, as it doesn't start with a response
status line as specified by RFC 2616. It does, however, start
with a "Status" header as described by the CGI spec. It
@@ -370,22 +399,18 @@
# "non-parsed header" mode, where the CGI script is responsible
# for generating a complete HTTP response with no help from the
# server.
- flush_output = not self.buffered and hasattr(file, 'flush')
+ flush_output = not self.buffered and hasattr(output, 'flush')
for name, value in self.generate_headers():
- file.write("%s: %s\r\n" % (name, value))
- file.write("\r\n")
- if self.body is not None:
- if isinstance(self.body, Stream):
- for chunk in self.body:
- if isinstance(chunk, unicode):
- chunk = chunk.encode(self.charset)
- file.write(chunk)
- if flush_output:
- file.flush()
- else:
- file.write(self.body)
+ output.write("%s: %s\r\n" % (name, value))
+ output.write("\r\n")
if flush_output:
- file.flush()
+ output.flush()
+ for chunk in self.generate_body_chunks():
+ output.write(chunk)
+ if flush_output:
+ output.flush()
+ if flush_output:
+ output.flush()
class Stream:
Modified: trunk/quixote/publish.py
===================================================================
--- trunk/quixote/publish.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/publish.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -7,8 +7,8 @@
__revision__ = "$Id$"
-import sys, os, traceback, cStringIO
-import time, types, re, warnings
+import sys, traceback, cStringIO
+import time, re
import struct
import urlparse
import cgitb
@@ -20,7 +20,6 @@
from quixote.directory import Directory
from quixote.errors import PublishError, TrailingSlashError, \
format_publish_error
-from quixote.html import htmltext
from quixote import util
from quixote.config import Config
from quixote.http_request import HTTPRequest
@@ -248,14 +247,14 @@
_SLASH_PAT = re.compile("//*")
- def try_publish(self, request, path):
- """try_publish(request : HTTPRequest, path : string) -> string
+ def try_publish(self, request):
+ """(request : HTTPRequest) -> object
- The master method that does all the work for a single request. Uses
- traverse_url() to get a callable object. The object is called and
- the output is returned. Exceptions are handled by the caller.
+ The master method that does all the work for a single request.
+ Exceptions are handled by the caller.
"""
self.start_request()
+ path = request.get_environ('PATH_INFO', '/')
# split path into components
if '//' in path:
path = self._SLASH_PAT.sub("/", path)
@@ -304,8 +303,8 @@
output = self.compress_output(request, str(output))
return output
- def process_request(self, request, env):
- """process_request(request : HTTPRequest, env : dict) : string
+ def process_request(self, request):
+ """(request : HTTPRequest) -> HTTPResponse
Process a single request, given an HTTPRequest object. The
try_publish() method will be called to do the work and
@@ -315,7 +314,7 @@
start_time = time.time()
try:
self.parse_request(request)
- output = self.try_publish(request, env.get('PATH_INFO', ''))
+ output = self.try_publish(request)
except PublishError, exc:
if (self.config.fix_trailing_slash and
isinstance(exc, TrailingSlashError) and
@@ -333,60 +332,12 @@
output = self.finish_failed_request()
output = self.filter_output(request, output)
self.logger.log_request(request, start_time)
- return output
-
- def publish(self, stdin, stdout, stderr, env):
- """publish(stdin : file, stdout : file, stderr : file, env : dict)
-
- Create an HTTPRequest object from the environment and from
- standard input, process it, and write the response to standard
- output.
- """
- request = HTTPRequest(stdin, env)
- output = self.process_request(request, env)
-
- # Output results from Response object
if output:
request.response.set_body(output)
- try:
- request.response.write(stdout)
- except IOError, exc:
- self.log('IOError caught while writing request (%s)' % exc)
self._clear_request()
+ return request.response
- def publish_cgi(self):
- """publish_cgi()
-
- Entry point from CGI scripts; it will execute the publish function
- once and return.
- """
- if sys.platform == "win32":
- # on Windows, stdin and stdout are in text mode by default
- import msvcrt
- msvcrt.setmode(sys.__stdin__.fileno(), os.O_BINARY)
- msvcrt.setmode(sys.__stdout__.fileno(), os.O_BINARY)
- self.publish(sys.__stdin__, sys.__stdout__, sys.__stderr__, os.environ)
-
- def publish_fcgi(self):
- """publish_fcgi()
-
- Entry point from FCGI scripts; it will repeatedly do the publish()
- function until there are no more requests. This should also work
- for CGI scripts but it is not as portable as publish_cgi().
- """
- from quixote import fcgi
- while fcgi.isFCGI() and not self.exit_now:
- f = fcgi.FCGI()
- self.publish(f.inp, f.out, f.err, f.env)
- f.Finish()
- if self.config.run_once:
- break
-
-
-# class Publisher
-
-
class SessionPublisher(Publisher):
def __init__(self, root_directory, session_mgr=None, **kwargs):
Modified: trunk/quixote/server/__init__.py
===================================================================
--- trunk/quixote/server/__init__.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/__init__.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -1,10 +1,5 @@
-"""quixote.server
+"""$URL$
+$Id$
-This package is for HTTP servers, built using one or another
-framework, that publish a Quixote application. These servers can make
-it easy to run a small application without having to install and
-configure a full-blown Web server such as Apache.
-
+This package is for Quixote/HTTP server glue.
"""
-
-__revision__ = "$Id$"
Copied: trunk/quixote/server/_fcgi.py (from rev 25457, trunk/quixote/fcgi.py)
===================================================================
--- trunk/quixote/fcgi.py 2004-10-26 17:43:03 UTC (rev 25457)
+++ trunk/quixote/server/_fcgi.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,461 @@
+#------------------------------------------------------------------------
+# Copyright (c) 1998 by Total Control Software
+# All Rights Reserved
+#------------------------------------------------------------------------
+#
+# Module Name: fcgi.py
+#
+# Description: Handles communication with the FastCGI module of the
+# web server without using the FastCGI developers kit, but
+# will also work in a non-FastCGI environment, (straight CGI.)
+# This module was originally fetched from someplace on the
+# Net (I don't remember where and I can't find it now...) and
+# has been significantly modified to fix several bugs, be more
+# readable, more robust at handling large CGI data and return
+# document sizes, and also to fit the model that we had previously
+# used for FastCGI.
+#
+# WARNING: If you don't know what you are doing, don't tinker with this
+# module!
+#
+# Creation Date: 1/30/98 2:59:04PM
+#
+# License: This is free software. You may use this software for any
+# purpose including modification/redistribution, so long as
+# this header remains intact and that you do not claim any
+# rights of ownership or authorship of this software. This
+# software has been tested, but no warranty is expressed or
+# implied.
+#
+#------------------------------------------------------------------------
+
+__revision__ = "$Id$"
+
+
+import os, sys, string, socket, errno, struct
+from cStringIO import StringIO
+import cgi
+
+#---------------------------------------------------------------------------
+
+# Set various FastCGI constants
+# Maximum number of requests that can be handled
+FCGI_MAX_REQS=1
+FCGI_MAX_CONNS = 1
+
+# Supported version of the FastCGI protocol
+FCGI_VERSION_1 = 1
+
+# Boolean: can this application multiplex connections?
+FCGI_MPXS_CONNS=0
+
+# Record types
+FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3
+FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6
+FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9
+FCGI_GET_VALUES_RESULT = 10
+FCGI_UNKNOWN_TYPE = 11
+FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
+
+# Types of management records
+ManagementTypes = [FCGI_GET_VALUES]
+
+FCGI_NULL_REQUEST_ID = 0
+
+# Masks for flags component of FCGI_BEGIN_REQUEST
+FCGI_KEEP_CONN = 1
+
+# Values for role component of FCGI_BEGIN_REQUEST
+FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3
+
+# Values for protocolStatus component of FCGI_END_REQUEST
+FCGI_REQUEST_COMPLETE = 0 # Request completed nicely
+FCGI_CANT_MPX_CONN = 1 # This app can't multiplex
+FCGI_OVERLOADED = 2 # New request rejected; too busy
+FCGI_UNKNOWN_ROLE = 3 # Role value not known
+
+
+error = 'fcgi.error'
+
+
+#---------------------------------------------------------------------------
+
+# The following function is used during debugging; it isn't called
+# anywhere at the moment
+
+def error(msg):
+ "Append a string to /tmp/err"
+ errf = open('/tmp/err', 'a+')
+ errf.write(msg+'\n')
+ errf.close()
+
+#---------------------------------------------------------------------------
+
+class record:
+ "Class representing FastCGI records"
+ def __init__(self):
+ self.version = FCGI_VERSION_1
+ self.recType = FCGI_UNKNOWN_TYPE
+ self.reqId = FCGI_NULL_REQUEST_ID
+ self.content = ""
+
+ #----------------------------------------
+ def readRecord(self, sock, unpack=struct.unpack):
+ (self.version, self.recType, self.reqId, contentLength,
+ paddingLength) = unpack(">BBHHBx", sock.recv(8))
+
+ content = ""
+ while len(content) < contentLength:
+ content = content + sock.recv(contentLength - len(content))
+ self.content = content
+
+ if paddingLength != 0:
+ padding = sock.recv(paddingLength)
+
+ # Parse the content information
+ if self.recType == FCGI_BEGIN_REQUEST:
+ (self.role, self.flags) = unpack(">HB", content[:3])
+
+ elif self.recType == FCGI_UNKNOWN_TYPE:
+ self.unknownType = ord(content[0])
+
+ elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
+ self.values = {}
+ pos = 0
+ while pos < len(content):
+ name, value, pos = readPair(content, pos)
+ self.values[name] = value
+
+ elif self.recType == FCGI_END_REQUEST:
+ (self.appStatus, self.protocolStatus) = unpack(">IB", content[0:5])
+
+ #----------------------------------------
+ def writeRecord(self, sock, pack=struct.pack):
+ content = self.content
+ if self.recType == FCGI_BEGIN_REQUEST:
+ content = pack(">HBxxxxx", self.role, self.flags)
+
+ elif self.recType == FCGI_UNKNOWN_TYPE:
+ content = pack(">Bxxxxxx", self.unknownType)
+
+ elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS:
+ content = ""
+ for i in self.values.keys():
+ content = content + writePair(i, self.values[i])
+
+ elif self.recType == FCGI_END_REQUEST:
+ content = pack(">IBxxx", self.appStatus, self.protocolStatus)
+
+ cLen = len(content)
+ eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary
+ padLen = eLen - cLen
+
+ hdr = pack(">BBHHBx", self.version, self.recType, self.reqId, cLen,
+ padLen)
+
+ ##debug.write('Sending fcgi record: %s\n' % repr(content[:50]) )
+ sock.send(hdr + content + padLen*'\000')
+
+#---------------------------------------------------------------------------
+
+_lowbits = ~(1L << 31) # everything but the 31st bit
+
+def readPair(s, pos):
+ nameLen = ord(s[pos]) ; pos = pos+1
+ if nameLen & 128:
+ pos = pos + 3
+ nameLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
+ valueLen = ord(s[pos]) ; pos = pos+1
+ if valueLen & 128:
+ pos = pos + 3
+ valueLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits)
+ return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen],
+ pos+nameLen+valueLen )
+
+#---------------------------------------------------------------------------
+
+_highbit = (1L << 31)
+
+def writePair(name, value):
+ l = len(name)
+ if l < 128:
+ s = chr(l)
+ else:
+ s = struct.pack(">I", l | _highbit)
+ l = len(value)
+ if l < 128:
+ s = s + chr(l)
+ else:
+ s = s + struct.pack(">I", l | _highbit)
+ return s + name + value
+
+#---------------------------------------------------------------------------
+
+def HandleManTypes(r, conn):
+ if r.recType == FCGI_GET_VALUES:
+ r.recType = FCGI_GET_VALUES_RESULT
+ v = {}
+ vars = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS,
+ 'FCGI_MAX_REQS' : FCGI_MAX_REQS,
+ 'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS}
+ for i in r.values.keys():
+ if vars.has_key(i): v[i] = vars[i]
+ r.values = vars
+ r.writeRecord(conn)
+
+#---------------------------------------------------------------------------
+#---------------------------------------------------------------------------
+
+
+_isFCGI = 1 # assume it is until we find out for sure
+
+def isFCGI():
+ return _isFCGI
+
+
+
+#---------------------------------------------------------------------------
+
+
+_init = None
+_sock = None
+
+class FCGI:
+ def __init__(self):
+ self.haveFinished = 0
+ if _init == None:
+ _startup()
+ if not _isFCGI:
+ self.haveFinished = 1
+ self.inp = sys.__stdin__
+ self.out = sys.__stdout__
+ self.err = sys.__stderr__
+ self.env = os.environ
+ return
+
+ if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'):
+ good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',')
+ good_addrs = map(string.strip, good_addrs) # Remove whitespace
+ else:
+ good_addrs = None
+
+ self.conn, addr = _sock.accept()
+ stdin, data = "", ""
+ self.env = {}
+ self.requestId = 0
+ remaining = 1
+
+ # Check if the connection is from a legal address
+ if good_addrs != None and addr not in good_addrs:
+ raise error, 'Connection from invalid server!'
+
+ while remaining:
+ r = record()
+ r.readRecord(self.conn)
+
+ if r.recType in ManagementTypes:
+ HandleManTypes(r, self.conn)
+
+ elif r.reqId == 0:
+ # Oh, poopy. It's a management record of an unknown
+ # type. Signal the error.
+ r2 = record()
+ r2.recType = FCGI_UNKNOWN_TYPE
+ r2.unknownType = r.recType
+ r2.writeRecord(self.conn)
+ continue # Charge onwards
+
+ # Ignore requests that aren't active
+ elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST:
+ continue
+
+ # If we're already doing a request, ignore further BEGIN_REQUESTs
+ elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0:
+ continue
+
+ # Begin a new request
+ if r.recType == FCGI_BEGIN_REQUEST:
+ self.requestId = r.reqId
+ if r.role == FCGI_AUTHORIZER: remaining = 1
+ elif r.role == FCGI_RESPONDER: remaining = 2
+ elif r.role == FCGI_FILTER: remaining = 3
+
+ elif r.recType == FCGI_PARAMS:
+ if r.content == "":
+ remaining = remaining-1
+ else:
+ for i in r.values.keys():
+ self.env[i] = r.values[i]
+
+ elif r.recType == FCGI_STDIN:
+ if r.content == "":
+ remaining = remaining-1
+ else:
+ stdin = stdin+r.content
+
+ elif r.recType == FCGI_DATA:
+ if r.content == "":
+ remaining = remaining-1
+ else:
+ data = data+r.content
+ # end of while remaining:
+
+ self.inp = StringIO(stdin)
+ self.err = StringIO()
+ self.out = StringIO()
+ self.data = StringIO(data)
+
+ def __del__(self):
+ self.Finish()
+
+ def Finish(self, status=0):
+ if not self.haveFinished:
+ self.haveFinished = 1
+
+ self.err.seek(0,0)
+ self.out.seek(0,0)
+
+ ##global debug
+ ##debug = open("/tmp/quixote-debug.log", "a+")
+ ##debug.write("fcgi.FCGI.Finish():\n")
+
+ r = record()
+ r.recType = FCGI_STDERR
+ r.reqId = self.requestId
+ data = self.err.read()
+ ##debug.write(" sending stderr (%s)\n" % `self.err`)
+ ##debug.write(" data = %s\n" % `data`)
+ while data:
+ chunk, data = self.getNextChunk(data)
+ ##debug.write(" chunk, data = %s, %s\n" % (`chunk`, `data`))
+ r.content = chunk
+ r.writeRecord(self.conn)
+ r.content = ""
+ r.writeRecord(self.conn) # Terminate stream
+
+ r.recType = FCGI_STDOUT
+ data = self.out.read()
+ ##debug.write(" sending stdout (%s)\n" % `self.out`)
+ ##debug.write(" data = %s\n" % `data`)
+ while data:
+ chunk, data = self.getNextChunk(data)
+ r.content = chunk
+ r.writeRecord(self.conn)
+ r.content = ""
+ r.writeRecord(self.conn) # Terminate stream
+
+ r = record()
+ r.recType = FCGI_END_REQUEST
+ r.reqId = self.requestId
+ r.appStatus = status
+ r.protocolStatus = FCGI_REQUEST_COMPLETE
+ r.writeRecord(self.conn)
+ self.conn.close()
+
+ #debug.close()
+
+
+ def getFieldStorage(self):
+ method = 'GET'
+ if self.env.has_key('REQUEST_METHOD'):
+ method = string.upper(self.env['REQUEST_METHOD'])
+ if method == 'GET':
+ return cgi.FieldStorage(environ=self.env, keep_blank_values=1)
+ else:
+ return cgi.FieldStorage(fp=self.inp,
+ environ=self.env,
+ keep_blank_values=1)
+
+ def getNextChunk(self, data):
+ chunk = data[:8192]
+ data = data[8192:]
+ return chunk, data
+
+
+Accept = FCGI # alias for backwards compatibility
+#---------------------------------------------------------------------------
+
+def _startup():
+ global _isFCGI, _init, _sock
+ # This function won't work on Windows at all.
+ if sys.platform[:3] == 'win':
+ _isFCGI = 0
+ return
+
+ _init = 1
+ try:
+ s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET,
+ socket.SOCK_STREAM)
+ s.getpeername()
+ except socket.error, (err, errmsg):
+ if err != errno.ENOTCONN: # must be a non-fastCGI environment
+ _isFCGI = 0
+ return
+
+ _sock = s
+
+
+#---------------------------------------------------------------------------
+
+def _test():
+ counter = 0
+ try:
+ while isFCGI():
+ req = Accept()
+ counter = counter+1
+
+ try:
+ fs = req.getFieldStorage()
+ size = string.atoi(fs['size'].value)
+ doc = ['*' * size]
+ except:
+ doc = ['<HTML><HEAD>'
+ '<TITLE>FCGI TestApp</TITLE>'
+ '</HEAD>\n<BODY>\n']
+ doc.append('<H2>FCGI TestApp</H2><P>')
+ doc.append('<b>request count</b> = %d<br>' % counter)
+ doc.append('<b>pid</b> = %s<br>' % os.getpid())
+ if req.env.has_key('CONTENT_LENGTH'):
+ cl = string.atoi(req.env['CONTENT_LENGTH'])
+ doc.append('<br><b>POST data (%s):</b><br><pre>' % cl)
+ keys = fs.keys()
+ keys.sort()
+ for k in keys:
+ val = fs[k]
+ if type(val) == type([]):
+ doc.append(' <b>%-15s :</b> %s\n'
+ % (k, val))
+ else:
+ doc.append(' <b>%-15s :</b> %s\n'
+ % (k, val.value))
+ doc.append('</pre>')
+
+
+ doc.append('<P><HR><P><pre>')
+ keys = req.env.keys()
+ keys.sort()
+ for k in keys:
+ doc.append('<b>%-20s :</b> %s\n' % (k, req.env[k]))
+ doc.append('\n</pre><P><HR>\n')
+ doc.append('</BODY></HTML>\n')
+
+
+ doc = string.join(doc, '')
+ req.out.write('Content-length: %s\r\n'
+ 'Content-type: text/html\r\n'
+ 'Cache-Control: no-cache\r\n'
+ '\r\n'
+ % len(doc))
+ req.out.write(doc)
+
+ req.Finish()
+ except:
+ import traceback
+ f = open('traceback', 'w')
+ traceback.print_exc( file = f )
+# f.write('%s' % doc)
+
+if __name__ == '__main__':
+ #import pdb
+ #pdb.run('_test()')
+ _test()
Added: trunk/quixote/server/cgi_server.py
===================================================================
--- trunk/quixote/server/cgi_server.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/cgi_server.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+"""$URL$
+$Id$
+"""
+
+import sys
+import os
+from quixote.http_request import HTTPRequest
+
+def run(create_publisher):
+ if sys.platform == "win32":
+ # on Windows, stdin and stdout are in text mode by default
+ import msvcrt
+ msvcrt.setmode(sys.__stdin__.fileno(), os.O_BINARY)
+ msvcrt.setmode(sys.__stdout__.fileno(), os.O_BINARY)
+ publisher = create_publisher()
+ request = HTTPRequest(sys.__stdin__, os.environ)
+ response = publisher.process_request(request)
+ try:
+ response.write(sys.__stdout__)
+ except IOError, err:
+ publisher.log("IOError while sending response ignored: %s" % err)
+
+
+if __name__ == '__main__':
+ from quixote.demo import create_publisher
+ run(create_publisher)
Property changes on: trunk/quixote/server/cgi_server.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ HeadURL Id
Added: trunk/quixote/server/fastcgi_server.py
===================================================================
--- trunk/quixote/server/fastcgi_server.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/fastcgi_server.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+"""$URL$
+$Id$
+
+Server for Quixote applications that use FastCGI. It should work
+for CGI too but the cgi_server module is preferred as it is more
+portable.
+"""
+
+from quixote.server import _fcgi
+from quixote.http_request import HTTPRequest
+
+def run(create_publisher):
+ publisher = create_publisher()
+ while _fcgi.isFCGI():
+ f = _fcgi.FCGI()
+ request = HTTPRequest(f.inp, f.env)
+ response = publisher.process_request(request)
+ try:
+ response.write(f.out)
+ except IOError, err:
+ publisher.log("IOError while sending response ignored: %s" % err)
+ f.Finish()
+
+
+if __name__ == '__main__':
+ from quixote.demo import create_publisher
+ run(create_publisher)
Property changes on: trunk/quixote/server/fastcgi_server.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ HeadURL Id
Deleted: trunk/quixote/server/medusa_http.py
===================================================================
--- trunk/quixote/server/medusa_http.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/medusa_http.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -1,143 +0,0 @@
-#!/usr/bin/env python
-
-"""quixote.server.medusa_http
-
-An HTTP handler for Medusa that publishes a Quixote application.
-"""
-
-__revision__ = "$Id$"
-
-# A simple HTTP server, using Medusa, that publishes a Quixote application.
-
-import sys
-import asyncore, rfc822, socket, urllib
-from StringIO import StringIO
-from medusa import http_server, xmlrpc_handler
-from quixote.http_request import HTTPRequest
-from quixote.http_response import Stream
-from quixote.publish import Publisher
-
-
-class StreamProducer:
- def __init__(self, stream):
- self.iterator = iter(stream)
-
- def more(self):
- try:
- return self.iterator.next()
- except StopIteration:
- return ''
-
-
-class QuixoteHandler:
- def __init__(self, publisher, server_name, server):
- """QuixoteHandler(publisher:Publisher, server_name:string,
- server:medusa.http_server.http_server)
-
- Publish the specified Quixote publisher. 'server_name' will
- be passed as the SERVER_NAME environment variable.
- """
- self.publisher = publisher
- self.server_name = server_name
- self.server = server
-
- def match(self, request):
- # Always match, since this is the only handler there is.
- return 1
-
- def handle_request(self, request):
- msg = rfc822.Message(StringIO('\n'.join(request.header)))
- length = int(msg.get('Content-Length', '0'))
- if length:
- request.collector = xmlrpc_handler.collector(self, request)
- else:
- self.continue_request('', request)
-
- def continue_request(self, data, request):
- msg = rfc822.Message(StringIO('\n'.join(request.header)))
- remote_addr, remote_port = request.channel.addr
- if '#' in request.uri:
- # MSIE is buggy and sometimes includes fragments in URLs
- [request.uri, fragment] = request.uri.split('#', 1)
- if '?' in request.uri:
- [path, query_string] = request.uri.split('?', 1)
- else:
- path = request.uri
- query_string = ''
-
- path = urllib.unquote(path)
- server_port = str(self.server.port)
- http_host = msg.get("Host")
- if http_host:
- if ":" in http_host:
- server_name, server_port = http_host.split(":", 1)
- else:
- server_name = http_host
- else:
- server_name = (self.server.ip or
- socket.gethostbyaddr(socket.gethostname())[0])
-
- environ = {'REQUEST_METHOD': request.command,
- 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''),
- 'CONTENT_TYPE': msg.get('Content-type', ''),
- 'CONTENT_LENGTH': len(data),
- "GATEWAY_INTERFACE": "CGI/1.1",
- 'PATH_INFO': path,
- 'QUERY_STRING': query_string,
- 'REMOTE_ADDR': remote_addr,
- 'REMOTE_PORT': str(remote_port),
- 'REQUEST_URI': request.uri,
- 'SCRIPT_NAME': '',
- "SCRIPT_FILENAME": '',
- 'SERVER_NAME': server_name,
- 'SERVER_PORT': server_port,
- 'SERVER_PROTOCOL': 'HTTP/1.1',
- 'SERVER_SOFTWARE': self.server_name,
- }
- for title, header in msg.items():
- envname = 'HTTP_' + title.replace('-', '_').upper()
- environ[envname] = header
-
- stdin = StringIO(data)
- qreq = HTTPRequest(stdin, environ)
- output = self.publisher.process_request(qreq, environ)
-
- qresponse = qreq.response
- if output:
- qresponse.set_body(output)
-
- # Copy headers from Quixote's HTTP response
- for name, value in qresponse.generate_headers():
- # XXX Medusa's HTTP request is buggy, and only allows unique
- # headers.
- request[name] = value
-
- request.response(qresponse.status_code)
-
- # XXX should we set a default Last-Modified time?
- if qresponse.body is not None:
- if isinstance(qresponse.body, Stream):
- request.push(StreamProducer(qresponse.body))
- else:
- request.push(qresponse.body)
-
- request.done()
-
-def main():
- from quixote import enable_ptl
- enable_ptl()
- from quixote.demo import DemoUI
-
- if len(sys.argv) == 2:
- port = int(sys.argv[1])
- else:
- port = 8080
- print 'Now serving the Quixote demo on port %d' % port
- server = http_server.http_server('', port)
- publisher = Publisher(DemoUI(), display_exceptions='plain')
- dh = QuixoteHandler(publisher, 'Quixote/demo', server)
- server.install_handler(dh)
- asyncore.loop()
-
-if __name__ == '__main__':
- main()
Copied: trunk/quixote/server/medusa_server.py (from rev 25457, trunk/quixote/server/medusa_http.py)
===================================================================
--- trunk/quixote/server/medusa_http.py 2004-10-26 17:43:03 UTC (rev 25457)
+++ trunk/quixote/server/medusa_server.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+"""$URL$
+$Id$
+
+An HTTP handler for Medusa that publishes a Quixote application.
+"""
+
+import asyncore, rfc822, socket, urllib
+from StringIO import StringIO
+from medusa import http_server, xmlrpc_handler
+from quixote.http_request import HTTPRequest
+
+
+class StreamProducer:
+ def __init__(self, chunks):
+ self.chunks = chunks # a generator
+
+ def more(self):
+ try:
+ return self.chunks.next()
+ except StopIteration:
+ return ''
+
+
+class QuixoteHandler:
+ def __init__(self, publisher, server):
+ self.publisher = publisher
+ self.server = server
+
+ def match(self, request):
+ # Always match, since this is the only handler there is.
+ return True
+
+ def handle_request(self, request):
+ msg = rfc822.Message(StringIO('\n'.join(request.header)))
+ length = int(msg.get('Content-Length', '0'))
+ if length:
+ request.collector = xmlrpc_handler.collector(self, request)
+ else:
+ self.continue_request('', request)
+
+ def continue_request(self, data, request):
+ msg = rfc822.Message(StringIO('\n'.join(request.header)))
+ remote_addr, remote_port = request.channel.addr
+ if '#' in request.uri:
+ # MSIE is buggy and sometimes includes fragments in URLs
+ [request.uri, fragment] = request.uri.split('#', 1)
+ if '?' in request.uri:
+ [path, query_string] = request.uri.split('?', 1)
+ else:
+ path = request.uri
+ query_string = ''
+
+ path = urllib.unquote(path)
+ server_port = str(self.server.port)
+ http_host = msg.get("Host")
+ if http_host:
+ if ":" in http_host:
+ server_name, server_port = http_host.split(":", 1)
+ else:
+ server_name = http_host
+ else:
+ server_name = (self.server.ip or
+ socket.gethostbyaddr(socket.gethostname())[0])
+
+ environ = {'REQUEST_METHOD': request.command,
+ 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''),
+ 'CONTENT_TYPE': msg.get('Content-type', ''),
+ 'CONTENT_LENGTH': len(data),
+ "GATEWAY_INTERFACE": "CGI/1.1",
+ 'PATH_INFO': path,
+ 'QUERY_STRING': query_string,
+ 'REMOTE_ADDR': remote_addr,
+ 'REMOTE_PORT': str(remote_port),
+ 'REQUEST_URI': request.uri,
+ 'SCRIPT_NAME': '',
+ "SCRIPT_FILENAME": '',
+ 'SERVER_NAME': server_name,
+ 'SERVER_PORT': server_port,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ 'SERVER_SOFTWARE': 'Quixote/2',
+ }
+ for title, header in msg.items():
+ envname = 'HTTP_' + title.replace('-', '_').upper()
+ environ[envname] = header
+
+ stdin = StringIO(data)
+ qrequest = HTTPRequest(stdin, environ)
+ qresponse = self.publisher.process_request(qrequest)
+
+ # Copy headers from Quixote's HTTP response
+ for name, value in qresponse.generate_headers():
+ # XXX Medusa's HTTP request is buggy, and only allows unique
+ # headers.
+ request[name] = value
+
+ request.response(qresponse.status_code)
+ request.push(StreamProducer(qresponse.generate_body_chunks()))
+ request.done()
+
+
+def run(create_publisher, host='', port=80):
+ server = http_server.http_server(host, port)
+ publisher = create_publisher()
+ handler = QuixoteHandler(publisher, server)
+ server.install_handler(handler)
+ asyncore.loop()
+
+
+if __name__ == '__main__':
+ from quixote.demo import create_publisher
+ run(create_publisher, port=8080)
Property changes on: trunk/quixote/server/medusa_server.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ HeadURL Id
Added: trunk/quixote/server/scgi_server.py
===================================================================
--- trunk/quixote/server/scgi_server.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/scgi_server.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+"""$URL$
+$Id$
+
+A SCGI server that uses Quixote to publish dynamic content.
+"""
+
+from scgi import scgi_server
+from quixote.http_request import HTTPRequest
+
+class QuixoteHandler(scgi_server.SCGIHandler):
+ def __init__(self, parent_fd, create_publisher, script_name=None):
+ scgi_server.SCGIHandler.__init__(self, parent_fd)
+ self.publisher = create_publisher()
+ self.script_name = script_name
+
+ def handle_connection(self, conn):
+ input = conn.makefile("r")
+ output = conn.makefile("w")
+ env = self.read_env(input)
+
+ if self.script_name is not None:
+ # mod_scgi doesn't know SCRIPT_NAME :-(
+ prefix = self.script_name
+ path = env['SCRIPT_NAME']
+ assert path[:len(prefix)] == prefix, (
+ "path %r doesn't start with script_name %r" % (path, prefix))
+ env['SCRIPT_NAME'] = prefix
+ env['PATH_INFO'] = path[len(prefix):] + env.get('PATH_INFO', '')
+
+ request = HTTPRequest(input, env)
+ response = self.publisher.process_request(request)
+ try:
+ response.write(output)
+ input.close()
+ output.close()
+ conn.close()
+ except IOError, err:
+ self.publisher.log("IOError while sending response "
+ "ignored: %s" % err)
+
+
+def run(create_publisher, host='', port=3000, script_name=None, max_children=5):
+ def create_handler(parent_fd):
+ return QuixoteHandler(parent_fd, create_publisher, script_name)
+ s = scgi_server.SCGIServer(create_handler, host=host, port=port,
+ max_children=max_children)
+ s.serve()
+
+
+if __name__ == '__main__':
+ from quixote.demo import create_publisher
+ run(create_publisher)
Property changes on: trunk/quixote/server/scgi_server.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ HeadURL Id
Deleted: trunk/quixote/server/twisted_http.py
===================================================================
--- trunk/quixote/server/twisted_http.py 2004-10-27 21:43:28 UTC (rev 25475)
+++ trunk/quixote/server/twisted_http.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -1,275 +0,0 @@
-#!/usr/bin/env python
-
-"""
-twist -- Demo of an HTTP server built on top of Twisted Python.
-"""
-
-__revision__ = "$Id: medusa_http.py 21221 2003-03-20 16:02:41Z akuchlin $"
-
-# based on qserv, created 2002/03/19, AMK
-# last mod 2003.03.24, Graham Fawcett
-# tested on Win32 / Twisted 0.18.0 / Quixote 0.6b5
-#
-# version 0.2 -- 2003.03.24 11:07 PM
-# adds missing support for session management, and for
-# standard Quixote response headers (expires, date)
-#
-# modified 2004/04/10 jsibre
-# better support for Streams
-# wraps output (whether Stream or not) into twisted type producer.
-# modified to use reactor instead of Application (Appication
-# has been deprecated)
-
-import urllib
-from twisted.protocols import http
-from twisted.web import server
-
-# imports for the TWProducer object
-from twisted.spread import pb
-from twisted.python import threadable
-from twisted.internet import abstract
-
-from quixote.http_request import HTTPRequest
-from quixote.http_response import Stream
-
-class QuixoteTWRequest(server.Request):
-
- def process(self):
- self.publisher = self.channel.factory.publisher
- environ = self.create_environment()
- # this seek is important, it doesn't work without it (it doesn't
- # matter for GETs, but POSTs will not work properly without it.)
- self.content.seek(0, 0)
- qxrequest = HTTPRequest(self.content, environ)
- self.quixote_publish(qxrequest, environ)
- resp = qxrequest.response
- self.setResponseCode(resp.status_code)
- for hdr, value in resp.generate_headers():
- self.setHeader(hdr, value)
- if resp.body is not None:
- TWProducer(resp.body, self)
- else:
- self.finish()
-
-
- def quixote_publish(self, qxrequest, env):
- """
- Warning, this sidesteps the Publisher.publish method,
- Hope you didn't override it...
- """
- pub = self.publisher
- output = pub.process_request(qxrequest, env)
-
- # don't write out the output, just set the response body
- # the calling method will do the rest.
- if output:
- qxrequest.response.set_body(output)
-
- pub._clear_request()
-
-
- def create_environment(self):
- """
- Borrowed heavily from twisted.web.twcgi
- """
- # Twisted doesn't decode the path for us,
- # so let's do it here. This is also
- # what medusa_http.py does, right or wrong.
- if '%' in self.path:
- self.path = urllib.unquote(self.path)
-
- serverName = self.getRequestHostname().split(':')[0]
- env = {"SERVER_SOFTWARE": server.version,
- "SERVER_NAME": serverName,
- "GATEWAY_INTERFACE": "CGI/1.1",
- "SERVER_PROTOCOL": self.clientproto,
- "SERVER_PORT": str(self.getHost()[2]),
- "REQUEST_METHOD": self.method,
- "SCRIPT_NAME": '',
- "SCRIPT_FILENAME": '',
- "REQUEST_URI": self.uri,
- "HTTPS": (self.isSecure() and 'on') or 'off',
- "ACCEPT_ENCODING": self.getHeader('Accept-encoding'),
- 'CONTENT_TYPE': self.getHeader('Content-type'),
- 'HTTP_COOKIE': self.getHeader('Cookie'),
- 'HTTP_REFERER': self.getHeader('Referer'),
- 'HTTP_USER_AGENT': self.getHeader('User-agent'),
- 'SERVER_PROTOCOL': 'HTTP/1.1',
- }
-
- client = self.getClient()
- if client is not None:
- env['REMOTE_HOST'] = client
- ip = self.getClientIP()
- if ip is not None:
- env['REMOTE_ADDR'] = ip
- xx, xx, remote_port = self.transport.getPeer()
- env['REMOTE_PORT'] = remote_port
- env["PATH_INFO"] = self.path
-
- qindex = self.uri.find('?')
- if qindex != -1:
- env['QUERY_STRING'] = self.uri[qindex+1:]
- else:
- env['QUERY_STRING'] = ''
-
- # Propogate HTTP headers
- for title, header in self.getAllHeaders().items():
- envname = title.replace('-', '_').upper()
- if title not in ('content-type', 'content-length'):
- envname = "HTTP_" + envname
- env[envname] = header
-
- return env
-
-
-class TWProducer(pb.Viewable):
- """
- A class to represent the transfer of data over the network.
-
- JES Note: This has more stuff in it than is minimally neccesary.
- However, since I'm no twisted guru, I built this by modifing
- twisted.web.static.FileTransfer. FileTransfer has stuff in it
- that I don't really understand, but know that I probably don't
- need. I'm leaving it in under the theory that if anyone ever
- needs that stuff (e.g. because they're running with multiple
- threads) it'll be MUCH easier for them if I had just left it in
- than if they have to figure out what needs to be in there.
- Furthermore, I notice no performance penalty for leaving it in.
- """
- request = None
- def __init__(self, data, request):
- self.request = request
- self.data = ""
- self.size = 0
- self.stream = None
- self.streamIter = None
-
- self.outputBufferSize = abstract.FileDescriptor.bufferSize
-
- if isinstance(data, Stream): # data could be a Stream
- self.stream = data
- self.streamIter = iter(data)
- self.size = data.length
- elif data: # data could be a string
- self.data = data
- self.size = len(data)
- else: # data could be None
- # We'll just leave self.data as ""
- pass
-
- request.registerProducer(self, 0)
-
-
- def resumeProducing(self):
- """
- This is twisted's version of a producer's '.more()', or
- an iterator's '.next()'. That is, this function is
- responsible for returning some content.
- """
- if not self.request:
- return
-
- if self.stream:
- # If we were provided a Stream, let's grab some data
- # and push it into our data buffer
-
- buffer = [self.data]
- bytesInBuffer = len(buffer[-1])
- while bytesInBuffer < self.outputBufferSize:
- try:
- buffer.append(self.streamIter.next())
- bytesInBuffer += len(buffer[-1])
- except StopIteration:
- # We've exhausted the Stream, time to clean up.
- self.stream = None
- self.streamIter = None
- break
- self.data = "".join(buffer)
-
- if self.data:
- chunkSize = min(self.outputBufferSize, len(self.data))
- data, self.data = self.data[:chunkSize], self.data[chunkSize:]
- else:
- data = ""
-
- if data:
- self.request.write(data)
-
- if not self.data:
- self.request.unregisterProducer()
- self.request.finish()
- self.request = None
-
- def pauseProducing(self):
- pass
-
- def stopProducing(self):
- self.data = ""
- self.request = None
- self.stream = None
- self.streamIter = None
-
- # Remotely relay producer interface.
-
- def view_resumeProducing(self, issuer):
- self.resumeProducing()
-
- def view_pauseProducing(self, issuer):
- self.pauseProducing()
-
- def view_stopProducing(self, issuer):
- self.stopProducing()
-
- synchronized = ['resumeProducing', 'stopProducing']
-
-threadable.synchronize(TWProducer)
-
-
-
-class QuixoteFactory(http.HTTPFactory):
-
- def __init__(self, publisher):
- self.publisher = publisher
- http.HTTPFactory.__init__(self, None)
-
- def buildProtocol(self, addr):
- p = http.HTTPFactory.buildProtocol(self, addr)
- p.requestFactory = QuixoteTWRequest
- return p
-
-
-def Server(root_directory, http_port, **kwargs):
- from twisted.internet import reactor
- from quixote.publish import Publisher
-
- # If you want SSL, make sure you have OpenSSL,
- # uncomment the follownig, and uncomment the
- # listenSSL() call below.
-
- ##from OpenSSL import SSL
- ##class ServerContextFactory:
- ## def getContext(self):
- ## ctx = SSL.Context(SSL.SSLv23_METHOD)
- ## ctx.use_certificate_file('/path/to/pem/encoded/ssl_cert_file')
- ## ctx.use_privatekey_file('/path/to/pem/encoded/ssl_key_file')
- ## return ctx
-
- publisher = Publisher(root_directory, **kwargs)
- qf = QuixoteFactory(publisher)
-
- reactor.listenTCP(http_port, qf)
- ##reactor.listenSSL(http_port, qf, ServerContextFactory())
-
- return reactor
-
-
-def run(root_directory, port, **kwargs):
- app = Server(root_directory, port, **kwargs)
- app.run()
-
-
-if __name__ == '__main__':
- from quixote import enable_ptl
- enable_ptl()
- run('quixote.demo', 8080, display_exceptions='plain')
Copied: trunk/quixote/server/twisted_server.py (from rev 25457, trunk/quixote/server/twisted_http.py)
===================================================================
--- trunk/quixote/server/twisted_http.py 2004-10-26 17:43:03 UTC (rev 25457)
+++ trunk/quixote/server/twisted_server.py 2004-10-27 21:54:59 UTC (rev 25476)
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+"""$URL$
+$Id$
+
+An HTTP server for Twisted that publishes a Quixote application.
+"""
+
+import urllib
+from twisted.protocols import http
+from twisted.web import server
+from twisted.python import threadable
+from twisted.internet import reactor
+from quixote.http_request import HTTPRequest
+
+
+class QuixoteFactory(http.HTTPFactory):
+ def __init__(self, publisher):
+ self.publisher = publisher
+ http.HTTPFactory.__init__(self, None)
+
+ def buildProtocol(self, addr):
+ protocol = http.HTTPFactory.buildProtocol(self, addr)
+ protocol.requestFactory = QuixoteRequest
+ return protocol
+
+
+class QuixoteRequest(server.Request):
+ def process(self):
+ environ = self.create_environment()
+ # this seek is important, it doesn't work without it (it doesn't
+ # matter for GETs, but POSTs will not work properly without it.)
+ self.content.seek(0, 0)
+ qxrequest = HTTPRequest(self.content, environ)
+ qxresponse = self.channel.factory.publisher.process_request(qxrequest)
+ self.setResponseCode(qxresponse.status_code)
+ for name, value in qxresponse.generate_headers():
+ self.setHeader(name, value)
+ QuixoteProducer(qxresponse, self)
+
+ def create_environment(self):
+ """
+ Borrowed heavily from twisted.web.twcgi
+ """
+ # Twisted doesn't decode the path for us, so let's do it here.
+ if '%' in self.path:
+ self.path = urllib.unquote(self.path)
+
+ serverName = self.getRequestHostname().split(':')[0]
+ env = {"SERVER_SOFTWARE": server.version,
+ "SERVER_NAME": serverName,
+ "GATEWAY_INTERFACE": "CGI/1.1",
+ "SERVER_PROTOCOL": self.clientproto,
+ "SERVER_PORT": str(self.getHost()[2]),
+ "REQUEST_METHOD": self.method,
+ "SCRIPT_NAME": '',
+ "SCRIPT_FILENAME": '',
+ "REQUEST_URI": self.uri,
+ "HTTPS": (self.isSecure() and 'on') or 'off',
+ "ACCEPT_ENCODING": self.getHeader('Accept-encoding'),
+ 'CONTENT_TYPE': self.getHeader('Content-type'),
+ 'HTTP_COOKIE': self.getHeader('Cookie'),
+ 'HTTP_REFERER': self.getHeader('Referer'),
+ 'HTTP_USER_AGENT': self.getHeader('User-agent'),
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ }
+
+ client = self.getClient()
+ if client is not None:
+ env['REMOTE_HOST'] = client
+ ip = self.getClientIP()
+ if ip is not None:
+ env['REMOTE_ADDR'] = ip
+ _, _, remote_port = self.transport.getPeer()
+ env['REMOTE_PORT'] = remote_port
+ env["PATH_INFO"] = self.path
+
+ qindex = self.uri.find('?')
+ if qindex != -1:
+ env['QUERY_STRING'] = self.uri[qindex+1:]
+ else:
+ env['QUERY_STRING'] = ''
+
+ # Propogate HTTP headers
+ for title, header in self.getAllHeaders().items():
+ envname = title.replace('-', '_').upper()
+ if title not in ('content-type', 'content-length'):
+ envname = "HTTP_" + envname
+ env[envname] = header
+
+ return env
+
+
+class QuixoteProducer:
+ """
+ Produce the Quixote response for twisted.
+ """
+ def __init__(self, qxresponse, request):
+ self.request = request
+ self.size = qxresponse.get_content_length()
+ self.stream = qxresponse.generate_body_chunks()
+ request.registerProducer(self, 0)
+
+ def resumeProducing(self):
+ if self.request:
+ try:
+ chunk = self.stream.next()
+ except StopIteration:
+ self.request.unregisterProducer()
+ self.request.finish()
+ self.request = None
+ else:
+ self.request.write(chunk)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ self.request = None
+
+ synchronized = ['resumeProducing', 'stopProducing']
+
+threadable.synchronize(QuixoteProducer)
+
+
+def run(create_publisher, host='', port=80):
+ publisher = create_publisher()
+ factory = QuixoteFactory(publisher)
+ reactor.listenTCP(port, factory, interface=host)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ from quixote.demo import create_publisher
+ run(create_publisher, port=8080)
Property changes on: trunk/quixote/server/twisted_server.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ HeadURL Id