# Copyright (C) 2004 Scott W. Dunlop <sdunlop at users.sourceforge.net>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
import os, sys
from traceback import format_exception,print_exception
from BaseHTTPServer import HTTPServer,BaseHTTPRequestHandler
from urllib import quote_plus
from urllib import unquote_plus
from exceptions import AttributeError,ValueError
from cStringIO import StringIO
from types import StringType
from html import normalize
from errors import *
from Cookie import SimpleCookie
class Request( object ):
"""
An abstract base class containing the necessary code for parsing URL
requests that would normally be encountered by Cloud Wiki servers, and
tracks the information relevant to the request.
! Instance Properties:
* server -- refers to the server of origin for this request.
* query -- contains the portion of the URI that describes the gateway or node referenced by the request.
* action -- contains the portion of the URI that specifies the method of interaction with the node specified by
the query attribute. Not applicable for gateways.
* args -- contains a dictionary of key:value argument pairs that followed the ? portion of the request URI.
* user -- the user, as determined by the server.
* cookies -- a SimpleCookie instance constructed from the http request.
"""
server_version = "CloudWiki/2.0"
__slots__ = 'query', 'action', 'args', 'user', 'cookies'
def __init__( self ):
self.query = None
self.action = None
self.args = {}
self.user = None
self.cookies = SimpleCookie()
def getQuery( self ):
return self.query
def setQuery( self, query ):
self.query = query
return self
def getAction( self ):
return self.action
def setAction( self, action ):
self.action = action
return self
def getArgs( self ):
return self.args
def getArg( self, key, default=None ):
return self.args.get(key, default)
def setArgs( self, content ):
self.args = args
return self
def setArg( self, key, value ):
self.args[key] = value
return self
def getUser( self ):
return self.user
def setUser( self, user ):
self.user = user
return self
def getCookies( self ):
return self.cookies
def getCookie( self, key, default=None ):
try:
return self.getCookies().get(key).value
except AttributeError:
return default
def getHeader( self, key ):
return self.headers.get( key )
def normalizeQuery( self ):
"forces the query to match the case rules specified by {html : normalize}"
self.setQuery( normalize( self.getQuery() ) )
def parseContent( self ):
"parses POST request content"
self.parseUriArgs( self.readContent() )
def parseUriPath( self, uri ):
"parses the path component of the request URI to determine query and action"
steps = uri.split( "/" )
if len( steps ) < 2:
query, action = '', ''
elif len( steps ) == 2:
query, action = (steps[1] or ''), ''
elif len( steps ) == 3:
query, action = (steps[1] or ''), (steps[2] or '')
else:
raise InvalidRequestError( request = uri )
if query is not None:
query = uriunquote( query )
self.query = query
self.action = action
def parseUriArgs( self, uriArgs ):
"parses the argument component of the request URI to determine arguments"
for arg_pair in uriArgs.split('&'):
key_and_value = arg_pair.split( "=", 1 )
arg_key = key_and_value[0]
if( len( key_and_value ) == 2 ):
self.args[ arg_key ] = uriunquote( key_and_value[1] )
else:
self.args[ arg_key ] = ''
def sendErrorResponse( self, error ):
"Called when an error has occurred while processing the request"
self.send_response( response.getCode() )
self.send_header( "Content-type", "text/html" )
self.sendData( self, error.asHtml() )
class ServerRequest( Request, BaseHTTPRequestHandler ):
def __init__( self, *args, **kwargs ):
Request.__init__( self )
BaseHTTPRequestHandler.__init__( self, *args, **kwargs )
def do_HEAD( self ):
"responds to HEAD requests"
try:
self.parseUri( )
self.parseCookies( )
response = self.getWiki().respondTo( self )
except CloudWiki, err:
self.sendErrorResponse( err )
else:
self.sendResponseHeaders( response )
def do_GET( self ):
"responds to GET requests"
try:
self.parseUri( )
self.parseCookies( )
response = self.getWiki().respondTo( self )
except CloudWiki, err:
self.sendErrorResponse( err )
else:
self.sendResponseHeaders( response )
self.sendResponseContent( response )
def do_POST( self ):
"responds to POST requests"
try:
self.parseUri( )
self.parseContent( )
self.parseCookies( )
if not self.getAction():
self.setAction( self.getArgs().get( 'action', '' ) )
response = self.getWiki().respondTo( self )
except CloudWiki, err:
self.sendErrorResponse( response )
else:
self.sendResponseHeaders( response )
self.sendResponseContent( response )
def log_message( self, format, *args ):
"overrides {BaseHTTPServer : BaseHTTPRequestHandler log_message} to use {Server logLine}"
self.getServer().logLine(
"%s -- [%s] %s",
self.address_string(),
self.log_date_time_string(),
format%args
)
def getServer( self ):
return self.server
def getHeaders( self ):
"returns a {rfc822 : Headers} instance"
return self.headers
def getUri( self ):
"returns the request URI."
return self.path
def getWiki( self ):
"returns the {wiki : Wiki} instance targeted by the request."
#TODO: Implement in CgiRequest
return self.getServer().getWiki()
def getContentLength( self ):
hv = self.getHeader( "Content-Length" )
if hv == None: return 0
try:
hi = int( hv )
except ValueError:
return 0
else:
return hi
def readContent( self ):
content_left = self.getContentLength()
content = StringIO()
while content_left > 0:
next = self.rfile.read( content_left )
content_left -= len( next )
content.write( next )
return content.getvalue()
def sendResponseHeaders( self, response ):
"transmits the response code and headers"
self.send_response( response.getCode() )
for key, value in response.getHeaders():
self.send_header( key, value )
self.end_headers()
def sendResponseContent( self, response ):
"transmits the response content"
self.sendData( response.getContent() )
def sendLines( self, lines ):
"transmits a list of lines"
for line in lines:
self.sendLine( line )
def sendLine( self, line ):
"transmits a single line, appending a \\n"
self.sendData( line )
self.sendData( "\n" )
def sendData( self, data ):
"transmits data to the http connection"
self.wfile.write( data )
def parseCookies( self ):
"parses the cookies supplied by the request"
for header in self.getHeaders().getallmatchingheaders( 'Cookie' ):
self.cookies.load( header )
def parseUri( self ):
"parses the request URI into components understood by Cloud Wiki"
path_and_args = self.getUri().split( "?", 1 )
self.parseUriPath( path_and_args[ 0 ] )
if len( path_and_args ) == 2 :
self.parseUriArgs( path_and_args[1] )
class CgiRequest( Request ):
__slots__ = ( 'stdin', 'stdout', 'environ', 'wiki' )
def __init__( self, wiki, stdin = sys.stdin, stdout = sys.stdout, environ = os.environ ):
Request.__init__( self )
self.wiki = wiki
self.stdin = stdin
self.stdout = stdout
self.environ = environ
def getWiki( self ):
return self.wiki
def getContentLength( self ):
ev = self.environ.get( "CONTENT_LENGTH" )
if ev == None: return 0
try:
ei = int( ev )
except ValueError, exc:
return 0
else:
return ei
def readContent( self ):
"reads additional content from from stdin"
content_left = self.getContentLength()
content = StringIO()
stdin = self.stdin
while content_left > 0:
next = stdin.read( content_left )
content_left -= len( next )
content.write( next )
return content.getvalue()
def sendResponseHeaders( self, response ):
"transmits the response code and headers to stdout"
self.sendData( "Status: " )
self.sendLine( str( response.getCode() ) )
for key, value in response.getHeaders():
self.sendData( key )
self.sendData( ": " )
self.sendLine( str( value ) )
self.sendLine( "" )
def sendResponseContent( self, response ):
"transmits the response content to stdout"
self.sendData( response.getContent() )
def sendLine( self, line ):
self.sendData( line + "\n" )
def sendData( self, data ):
self.stdout.write( data )
def runHeadRequest( self ):
"responds to HEAD requests"
try:
self.parseUri( )
self.parseCookies( )
response = self.getWiki().respondTo( self )
except CloudError, err:
self.sendErrorResponse( err )
else:
self.sendResponseHeaders( response )
def runGetRequest( self ):
"responds to GET requests"
try:
self.parseUri( )
self.parseCookies( )
response = self.getWiki().respondTo( self )
except CloudError, err:
self.sendErrorResponse( err )
else:
self.sendResponseHeaders( response )
self.sendResponseContent( response )
def runPostRequest( self ):
"responds to POST requests"
try:
self.parseUri( )
self.parseContent( )
self.parseCookies( )
if not self.getAction():
self.setAction( self.getArgs().get( 'action', '' ) )
response = self.getWiki().respondTo( self )
except CloudError, err:
self.sendErrorResponse( err )
else:
self.sendResponseHeaders( response )
self.sendResponseContent( response )
def run( self ):
mt = self.environ.get( "REQUEST_METHOD" )
if mt == 'GET':
self.runGetRequest()
elif mt == 'POST':
self.runPostRequest()
elif mt == 'HEAD':
self.runHeadRequest()
def parseCookies( self ):
"parses the cookies supplied by the request"
self.cookies.load( self.environ.get( 'HTTP_COOKIE' ) or "" )
def parseUri( self ):
"parses the request URI into components understood by Cloud Wiki"
path_info = self.environ.get( "PATH_INFO" ) or ""
query_string = self.environ.get( "QUERY_STRING" ) or ""
self.parseUriPath( path_info )
self.parseUriArgs( query_string )
class Response( object ):
"""
Used to incrementally compose a response to a HTTP request by Cloud Wiki. This object is primarily
used just for storage, and provides very little actual logic.
! Instance Properties:
* code -- contains the http response code for this response; defaults to 200.
* cookie -- contains a list of cookies to be sent to the client, with a fixed lifespan of 3600 seconds.
* contentType -- contains a MIME type string. Defaults to "text/html"
* content -- either a StringIO object for buffering content prior to transmission, or a string containing
the entire content intended for transmission. Defaults to a StringIO object.
"""
__slots__ = ( "code", "cookies", "content", "contentType" )
def __init__( self, code = 200):
self.code = code
self.cookies = []
self.content = StringIO()
self.contentType = "text/html"
def setCode( self, code ):
self.code = code
def getCode( self ):
return self.code
def getContentType( self ):
return self.contentType
def setContentType( self, contentType ):
self.contentType = contentType
return self
def getCookies( self ):
for cookie in self.cookies:
yield cookie
def addCookie( self, key, value ):
self.cookies.append( ( key, value ) )
return self
def getContent( self ):
"returns the content of the response -- if the content attribute is a StringIO instance,"
" this returns the value of that instance."
if not isinstance( self.content, StringType ):
self.content = self.content.getvalue()
return self.content
def setContent( self, content ):
"wraps the supplied content in a StringIO instance and stores it in the content attribute."
self.content = StringIO( content )
return self
def addContent( self, content ):
"only usable when content is a StringIO object"
self.content.write( content )
return self
def getHeaders( self ):
"iterates over the response's headers"
yield "Content-type", self.getContentType()
yield "Response-length", len( self.getContent() )
for key, value in self.getCookies():
yield "Set-cookie", "%s=%s;Max-Age=3600" % ( key, value )
def write( self, data ):
"extends the content with the supplied data."
if self.content is None:
self.setContent( data )
else:
self.content.write( data )
return self
class Server( HTTPServer ):
"""
A derivative of {BaseHTTPServer : HTTPServer} that uses {ServerRequest}s
to decode incoming requests, relays them to the correct fragment for
rendering a {Response}, and maintains information relevant to the wiki
for serving HTTP requests.
! Instance Properties:
* wiki -- returns the {wiki : Wiki} instance associated with this server.
* port -- returns the port number. Defaults to, in order, wiki.getHttpPort() or 8080.
"""
__slots__ = (
'port', 'wiki'
)
def __init__( self, wiki, port = None ):
self.port = port or wiki.getHttpPort() or 8080
self.wiki = wiki
HTTPServer.__init__( self, ('', self.port), self.getHandlerClass() )
def getWiki( self ):
return self.wiki
def getPort( self ):
return self.port
def logData( self, format, *args ):
"a convenience function that instructs the wiki to log the supplied data"
self.getWiki().logData( format, *args )
def logLine( self, format, *args ):
"a convenience function that instructs the wiki to log the supplied line"
self.getWiki().logLine( format, *args )
def getHandlerClass( self ):
"returns the class that should be used for handling new requests"
return ServerRequest
def runForever( self ):
"instructs the server to consume the current thread in serving requests, and will not return"
self.serve_forever()
|