Description:This was written for an undergraduate course in Network Programming.
This particular piece of code was written in Python to implement a basic web server based solely on the published standards surrounding HTTP and similar protocols.
"""
HTTPython - A simple web server written in Python
Copyright (C) 2011 Jon Olson
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 3 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, see <http://www.gnu.org/licenses/>.
"""
"""
@author Jon Olson
@requires Python 2.7
"""
import SocketServer
class message( object ):
"""
The basic unit of HTTP communication, consisting of a structured sequence of octets matching the
syntax defined in RFC1945 Section 4 and transmitted via the connection.
"""
pass
class request( message ):
"""
An HTTP request message (as defined in RFC 1945 Section 5).
"""
pass
class response( message ):
"""
An HTTP response message (as defined in RFC 1945 Section 6).
"""
pass
class resource( object ):
"""
A network data object or service which can be identified by a URI (RFC 1945 Section 3.2).
"""
pass
class entity( resource ):
"""
A particular representation or rendition of a data resource, or reply from a service resource,
that may be enclosed within a request or response message. An entity consists of metainformation
in the form of entity headers and content in the form of an entity body.
"""
pass
class client( object ):
"""
An application program that establishes connections for the purpose of sending requests.
"""
pass
class user_agent( client ):
"""
The client which initiates a request. These are often browsers, editors, spiders (web-traversing
robots), or other end user tools.
"""
pass
class ThreadedTCPServer( SocketServer.ThreadingMixIn, SocketServer.TCPServer ):
"""
Threading supported for a server instance
"""
pass
class patterns( object ):
"""
Much of the content in this section is defined as part of RFC 1945
"""
OCTET = r'(?s).'
CHAR = r'\0-\x7F'
UPALPHA = r'A-Z'
LOALPHA = r'a-z'
ALPHA = LOALPHA + UPALPHA
DIGIT = r'0-9'
CTL = r'\0-\x1F\x7F'
CR = r'\x0D'
LF = r'\x0A'
SP = r'\x20'
HT = r'\x09'
QUOTE = r'\x22'
CRLF = CR + LF
#CRLF = '['+CR + LF+']'
#CRLF = r'\r\n'
LWS = '(?:(?:' + CRLF +')?[' + SP + HT + ']+)'
TEXT = r'(?:[^' + CTL + ']|' + LWS + ')'
HEX = DIGIT + r'A-Fa-f'
tspecials = r'\(\)<>@,;:\\"/\[\]\?={}' + SP + HT
token = r'(?:[^' + CTL + tspecials + r']+)'
# TEXT excluding "(" and ")"
ctext = r'(?:[^' + CTL + '()]|' + LWS + ')'
comment = r'\(' + ctext + r'*?\)'
# CHAR excluding '"', CTLs, but include LWS
qdtext = r'(?:[\x20\x21\x23-\x7E]|' + LWS + ')'
quoted_string = r'"' + qdtext + '*"'
word = '(?:' + token + '|' + quoted_string + ')'
# HTTP-Version
http_version = r'(?P<HTTP_Version>(?i)HTTP/[' + DIGIT + r']+\.[' + DIGIT + ']+)'
# URI
reserved = r';/\?:@&=\+'
extra = r"!\*'\(\),"
safe = r'\$_\.\-'
unsafe = r'(?:[' + CTL + SP + r'"#%<>])'
national = r'[^a-zA-Z0-9;/?:@&=+!*\'(),$_.\-\0-\x20\x7F"#%<>]'
escape = r'(?:%[' + HEX + '][' + HEX + '])'
unreserved = r'(?:[' + ALPHA + DIGIT + safe + extra + ']|' + national + ')'
uchar = r'(?:' + unreserved + '|' + escape + ')'
pchar = r'(?:' + uchar + '|[:@&=+])'
fragment = r'(?:' + uchar + '|[' + reserved + '])*'
query = fragment
net_loc = r'(?:' + pchar + '|[;?])*'
scheme = r'(?:[' + ALPHA + DIGIT + '\+\-\.]+)'
param = r'(?:' + pchar + '|[/])*'
params = r'(?:' + param + r'(?:[;]' + param + ')*)'
segment = r'(?:' + pchar + '*)'
fsegment = r'(?:' + pchar + '+)'
path = r'(?:' + fsegment + r'(?:[/]' + segment + ')*)'
rel_path = r'(?:' + path + '?(?:[;]' + params + ')?(?:[?]' + query + ')?)'
abs_path = r'(?:/' + rel_path + ')'
net_path = r'(?://' + net_loc + abs_path + '?)'
relativeURI = r'(?:' + net_path + '|' + abs_path + '|' + rel_path + ')'
absoluteURI = r'(?:' + scheme + ':' + r'(?:' + uchar + '|[' + reserved + '])*)'
URI = r'(?:(?:' + absoluteURI + '|' + relativeURI + ')(?:#' + fragment + ')?)'
# http URL
port = r'(?:[' + DIGIT + ']*)'
# from RFC 1123
ip4digit = r'(?:[1-9]?[0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
ip4 = ip4digit.join((r'(?:',r'\.',r'\.',r'\.',')'))
# from RFC 3513
ip6digit = r'(?:[' + HEX + ']{1,4})'
ip6 = ip6digit.join((r'(?:','(?::','){7}|::' + ip4 + '?|(?:',':){1,7}:|',':(?::',\
'){1,6}|(?:',':){2}(?::','){1,5}|(?:',':){3}(?::','){1,4}|(?:',':){4}(?::',\
'){1,3}|(?:',':){5}(?::','){1,2}|(?:',':){6}:','|:(?::','){1,7}|(?:(?:',\
':){6}|(?:',':){1,5}:|(?:',':){1,4}:',':|(?:',':){1,3}:(?::','){1,2}:|(?:',\
':){1,2}:(?::','){1,3}:|',':(?::','){1,5}:|:(?::','){1,4}:)' + ip4 + ')'))
specials = r'()<>@,;:\\".\[\]'
atom = r'(?:[^' + CTL + SP + specials + '\x80-\xFF]+)'
domainref = atom
LWS2 = '(?:(?:(?:' + CRLF + ')?(?:' + SP + '|' + HT + '))+)'
dtext = r'[^\[\]\\' + CR + '\x80-\xFF]' + LWS2
quotedpair = r'(?:\\[' + CHAR + '])'
domainlit = r'(?:\[(?:' + dtext + '|' + quotedpair + r')*\])'
LetDig = ALPHA + DIGIT
LetDigHyp = LetDig + r'\-'
LDHstr = r'(?:[' + LetDigHyp + ']+)'
label = r'(?:[' + ALPHA + r'](?:' + LDHstr + '?[' + LetDig + '])?)'
subdomain = r'(?:' + domainref + '|' + domainlit + '|' + label + ')'
domain = r'(?:' + subdomain + r'(?:\.' + subdomain + r')*| )'
host = r'(?:' + domain + '|' + ip4 + '|' + ip6 + ')'
http_url = r'(?:http://' + host + '(?::' + port + ')?' + abs_path + '?)'
# HTTP date
month = r'(?:J[au]n|Feb|Ma[ry]|Apr|Jul|Aug|Sep|Oct|Nov|Dec)'
wkday = r'(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
weekday = r'(?:(?:Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day)'
time = r'(?:(?:[0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])'
date1 = r'(?:[' + DIGIT +']{2}' + SP + month + SP + '[' + DIGIT + ']{4})'
date2 = r'(?:[' + DIGIT +']{2}-' + month + '-[' + DIGIT + ']{2})'
date3 = r'(?:' + month + SP + '(?:[' + DIGIT + ']|' + SP + ')[' + DIGIT + '])'
asctime_date = r'(?:' + wkday + SP + date3 + SP + time + SP + '[' + DIGIT + ']{4})'
rfc850_date = r'(?:' + weekday + ',' + SP + date2 + SP + time + SP + 'GMT)'
rfc1123_date = r'(?:' + wkday + ',' + SP + date1 + SP + time + SP + 'GMT)'
http_date = r'(?:' + rfc1123_date + '|' + rfc850_date + '|' + asctime_date + ')'
# media types
parameter = r'(?:' + token + r'=(?:' + token + '|' + quoted_string + '))'
media_type = r'(?:' + token + '/' + token + '(?:;' + parameter + ')*)'
# product tokens
product = r'(?:' + token + '(?:/' + token + ')?)'
products = r'(?:' + product + '(?:\s+' + product + ')*)'
# http messages
request_uri = r'(?:' + absoluteURI + '|' + abs_path + ')'
field_content = r'(?:' + TEXT + '*|(?:' + token + '|[' + tspecials + ']|' + quoted_string\
+ ')*)'
field_value = r'(?:(?:' + field_content + '|' + LWS + ')*)'
message_header = r'(?:' + token + r':(?:' + field_value + ')?)'
header_breakdown = r'(?:(?P<name>' + token + r'):\s*(?P<value>' + field_value + ')?)'
# HTTP/0.9 messages
simple_request = r'(?:GET' + SP + '(?P<Simple_Request>' + request_uri + ')' + CRLF + ')'
simple_response = r'(?:^' + OCTET + '*)'
# HTTP/1.0 messages
status_line = r'(?:' + http_version + SP + '[' + DIGIT + ']{3}' + SP + '(?:[^' + CTL\
+ ']|[' + SP + HT + ']+)*)'
request_line = r'(?P<Request_Line>(?P<REQUEST_METHOD>GET|HEAD|POST|' + token + ')' + SP\
+ '(?P<Request_URI>' + request_uri + ')' + SP + http_version + ')'
full_request = r'(?:' + request_line + CRLF + r'(?P<HEADERS>(?:' + message_header + CRLF\
+ ')*)' + CRLF + '(?P<Entity_Body>' + OCTET + '*))'
full_response = r'(?:' + status_line + CRLF + r'(?:' + message_header + CRLF\
+ ')*' + CRLF + '(?:' + OCTET + '*))'
http_request = r'(?:' + simple_request + '|' + full_request + ')'
http_response = r'(?:' + simple_response + '|' + full_response + ')'
http_message = r'(?si)^(?:' + simple_request + '|' + simple_response + '|' + full_request\
+ '|' + full_response + ')'
uri_breakdown = r'(?P<uri>(?P<absolute>(?P<scheme>' + scheme + ')://(?P<host>(?:' + label\
+ '(?:[.]' + label + ')*|' + ip4 + '|\[' + ip6 + '\]))(?::(?P<port>' + port\
+ '))?)?(?P<path>/' + path + '?)(?:[;](?P<parameters>' + params\
+ '))?(?:[?](?P<query>' + query + '))?(?:[#](?P<fragment>' + fragment\
+ '))?)'
import re
@staticmethod
def eq( pattern, against, flags = re.S | re.I ):
"""
Determine if the given pattern completely matches the given test
"""
return patterns.fullMatch( pattern, against, flags ) is not None
@staticmethod
def contains( pattern, against, flags = re.S | re.I ):
"""
Determine if the given pattern is contained in the given text
"""
return patterns.search( pattern, against, flags ) is not None
@staticmethod
def search( pattern, against, flags = re.S | re.I ):
"""
Determine if the given pattern is contained in the given text
"""
import re
return re.search( pattern, against, flags )
@staticmethod
def matches( pattern, against, flags = re.S | re.I ):
"""
Determines if the given pattern matches the beginning of the given text, returning the
matching portion
"""
import re
return re.match( pattern, against, flags )
@staticmethod
def fullMatch( pattern, against, flags = re.S | re.I ):
"""
Determines if the given pattern matches the entirety of the given text, returning the
details
"""
return patterns.matches( '^' + pattern + '$', against, flags )
@staticmethod
def headerIter( headerData, flags = re.S ):
"""
Used to break apart the headers in a loop. It returns an iterator
"""
import re
return re.finditer( patterns.header_breakdown, headerData, flags )
class header( object ):
"""
A header used for both request and response handling
"""
def __init__( self, name, contents = None ):
"""
Initialize the internal variables and assign the passed values using the properties
@param name The name of the header
@param contents The content of the header
"""
dbg(name = name, contents = contents)
self.__name = None
self.__contents = None
self.name = name
self.contents = contents
@property
def name( self ):
"""
name getter
"""
return self.__name
@name.setter
def name( self, value ):
"""
name setter
"""
if patterns.eq( patterns.token, value ):
self.__name = value
elif patterns.eq( patterns.message_header, value ):
m = patterns.fullMatch( patterns.header_breakdown, value )
self.__name = m.group('name')
self.contents = m.group('value')
else:
raise ValueError
@property
def contents( self ):
"""
contents getter
"""
return self.__contents
@contents.setter
def contents( self, value ):
"""
content setter
"""
if patterns.eq( patterns.field_value, value ):
self.__contents = value
else:
raise ValueError
def __str__( self ):
"""
string constructor
"""
if self.__contents is None:
return str( self.__name )
else:
return '{}: {}'.format( self.__name, self.__contents )
class ThreadedTCPHandler( SocketServer.StreamRequestHandler ):
"""
This class handles each request that is received by the server
"""
def handle( self ):
"""
This method performs the handling
"""
dbg('Got a request from', self.client_address)
import socket
data = ''
block = 2 ** 14
# read in the data sent from the client
timing = self.request.gettimeout()
self.request.settimeout(0.5)
# fetch data from the buffer until either it gets an incomplete block back, or the timeout
# happens
while True:
try:
data += self.request.recv( block )
if len(data) % block != 0:
break
except (socket.timeout):
break
self.request.settimeout(timing)
dbg('Request was {} bytes long.'.format(len(data)))
# check to see that it is, in fact, a standard request
m = patterns.fullMatch( patterns.http_request, data )
if m is None:
raise BadRequest, data
dbg('pattern test against the request passed!')
# extract necessary information from the request data
info = self.extractInfo( m.groupdict() )
info['version'] = info['version'].upper()
# attempt to handle the
try:
response = server.handle( **info )
response = info['version'] + ' ' + '\r\n'.join(map(str,response))
except HTTPStatusCode as e:
response = info['version'] + ' {} {}\r\n'.format(e.code,e.reason)
self.wfile.write( response )
def extractInfo( self, matchData ):
"""
Extract the details of the request.
"""
out = dict()
out['uri'] = matchData['Simple_Request']
if out['uri'] is None:
out['uri'] = self.extractURI( matchData['Request_URI'] )
out['method'] = matchData['REQUEST_METHOD'].lower()
out['version'] = matchData['HTTP_Version'].lower()
else:
out['method'] = 'get'
out['version'] = 'http/0.9'
out['headers'] = self.extractHeaders( matchData['HEADERS'] )
out['body'] = matchData['Entity_Body']
dbg('extracted info:', **out)
return out
def extractHeaders( self, headerData ):
"""
Breakdown the headers into their individual name-value pairs
"""
headers = list()
dbg(headerData = headerData)
for i in patterns.headerIter( headerData ):
dbg(**i.groupdict())
headers.append( header(i.group('name'), i.group('value')) )
return headers
def extractURI( self, uri ):
"""
Breakdown the request URI into its subcomponents, if possible
"""
m = patterns.fullMatch( patterns.uri_breakdown, uri )
# if for some reason it wasn't possible to parse the uri, we'll return it so the code
# can handle it elsewhere
if m is not None:
m = m.groupdict()
# ensure the port is properly specified
if m['port'] is None or m['port'] > 65535:
m['port'] = 80
return m
else:
return {'uri': uri}
class HTTPStatusCode( Exception ):
"""
These are used primarily to communicate the HTTP status codes as specified in RFC 1945
"""
def __init__( self, code, reason ):
"""
Initialize the exception
"""
Exception.__init__( self )
self.code = code
self.reason = reason
def __str__( self ):
"""
Produce a string version of the exception. This is used by the handling class as a means of
not needing to know details of the variables internal to this class.
"""
return '{} {}'.format(self.code, self.reason)
class Success( HTTPStatusCode ):
"""
2XX status codes fall under this category
From RFC 1945:
This class of status code indicates that the client's request was
successfully received, understood, and accepted.
"""
pass
class OK( Success ):
"""
From RFC 1945:
The request has succeeded. The information returned with the
response is dependent on the method used in the request, as follows:
GET an entity corresponding to the requested resource is sent
in the response;
HEAD the response must only contain the header information and
no Entity-Body;
POST an entity describing or containing the result of the action.
"""
def __init__( self, code = 200, reason = 'OK' ):
Success.__init__( self, code, reason )
class Created( Success ):
"""
From RFC 1945:
The request has been fulfilled and resulted in a new resource being
created. The newly created resource can be referenced by the URI(s)
returned in the entity of the response. The origin server should
create the resource before using this Status-Code. If the action
cannot be carried out immediately, the server must include in the
response body a description of when the resource will be available;
otherwise, the server should respond with 202 (accepted).
Of the methods defined by this specification, only POST can create a
resource.
"""
def __init__( self, code = 201, reason = 'Created' ):
Success.__init__( self, code, reason )
class Accepted( Success ):
"""
The request has been accepted for processing, but the processing
has not been completed. The request may or may not eventually be
acted upon, as it may be disallowed when processing actually takes
place. There is no facility for re-sending a status code from an
asynchronous operation such as this.
The 202 response is intentionally non-committal. Its purpose is to
allow a server to accept a request for some other process (perhaps
a batch-oriented process that is only run once per day) without
requiring that the user agent's connection to the server persist
until the process is completed. The entity returned with this
response should include an indication of the request's current
status and either a pointer to a status monitor or some estimate of
when the user can expect the request to be fulfilled.
"""
def __init__( self, code = 202, reason = 'Accepted' ):
Success.__init__( self, code, reason )
class NoContent( Success ):
"""
The server has fulfilled the request but there is no new
information to send back. If the client is a user agent, it should
not change its document view from that which caused the request to
be generated. This response is primarily intended to allow input
for scripts or other actions to take place without causing a change
to the user agent's active document view. The response may include
new metainformation in the form of entity headers, which should
apply to the document currently in the user agent's active view.
"""
def __init__( self, code = 204, reason = 'No Content' ):
Success.__init__( self, code, reason )
class Redirection( HTTPStatusCode ):
"""
This class of status code indicates that further action needs to be
taken by the user agent in order to fulfill the request. The action
required may be carried out by the user agent without interaction
with the user if and only if the method used in the subsequent
request is GET or HEAD. A user agent should never automatically
redirect a request more than 5 times, since such redirections usually
indicate an infinite loop.
"""
pass
class MultipleChoices( Redirection ):
"""
This response code is not directly used by HTTP/1.0 applications,
but serves as the default for interpreting the 3xx class of
responses.
The requested resource is available at one or more locations.
Unless it was a HEAD request, the response should include an entity
containing a list of resource characteristics and locations from
which the user or user agent can choose the one most appropriate.
If the server has a preferred choice, it should include the URL in
a Location field; user agents may use this field value for
automatic redirection.
"""
def __init__( self, code = 300, reason = 'Multiple Choices' ):
Redirection.__init__( self, code, reason )
class MovedPermanently( Redirection ):
"""
The requested resource has been assigned a new permanent URL and
any future references to this resource should be done using that
URL. Clients with link editing capabilities should automatically
relink references to the Request-URI to the new reference returned
by the server, where possible.
The new URL must be given by the Location field in the response.
Unless it was a HEAD request, the Entity-Body of the response
should contain a short note with a hyperlink to the new URL.
If the 301 status code is received in response to a request using
the POST method, the user agent must not automatically redirect the
request unless it can be confirmed by the user, since this might
change the conditions under which the request was issued.
Note: When automatically redirecting a POST request after
receiving a 301 status code, some existing user agents will
erroneously change it into a GET request.
"""
def __init__( self, code = 301, reason = 'Moved Permanently' ):
Redirection.__init__( self, code, reason )
class MovedTemporarily( Redirection ):
"""
The requested resource resides temporarily under a different URL.
Since the redirection may be altered on occasion, the client should
continue to use the Request-URI for future requests.
The URL must be given by the Location field in the response. Unless
it was a HEAD request, the Entity-Body of the response should
contain a short note with a hyperlink to the new URI(s).
If the 302 status code is received in response to a request using
the POST method, the user agent must not automatically redirect the
request unless it can be confirmed by the user, since this might
change the conditions under which the request was issued.
Note: When automatically redirecting a POST request after
receiving a 302 status code, some existing user agents will
erroneously change it into a GET request.
"""
def __init__( self, code = 302, reason = 'Moved Temporarily' ):
Redirection.__init__( self, code, reason )
class NotModified( Redirection ):
"""
If the client has performed a conditional GET request and access is
allowed, but the document has not been modified since the date and
time specified in the If-Modified-Since field, the server must
respond with this status code and not send an Entity-Body to the
client. Header fields contained in the response should only include
information which is relevant to cache managers or which may have
changed independently of the entity's Last-Modified date. Examples
of relevant header fields include: Date, Server, and Expires. A
cache should update its cached entity to reflect any new field
values given in the 304 response.
"""
def __init__( self, code = 304, reason = 'Not Modified' ):
Redirection.__init__( self, code, reason )
class ClientError( HTTPStatusCode ):
"""
The 4xx class of status code is intended for cases in which the
client seems to have erred. If the client has not completed the
request when a 4xx code is received, it should immediately cease
sending data to the server. Except when responding to a HEAD request,
the server should include an entity containing an explanation of the
error situation, and whether it is a temporary or permanent
condition. These status codes are applicable to any request method.
Note: If the client is sending data, server implementations on TCP
should be careful to ensure that the client acknowledges receipt
of the packet(s) containing the response prior to closing the
input connection. If the client continues sending data to the
server after the close, the server's controller will send a reset
packet to the client, which may erase the client's unacknowledged
input buffers before they can be read and interpreted by the HTTP
application.
"""
class BadRequest( ClientError ):
"""
The request could not be understood by the server due to malformed
syntax. The client should not repeat the request without
modifications.
"""
def __init__( self, code = 400, reason = 'Bad Request' ):
ClientError.__init__( self, code, reason )
class Unauthorized( ClientError ):
"""
The request requires user authentication. The response must include
a WWW-Authenticate header field (Section 10.16) containing a
challenge applicable to the requested resource. The client may
repeat the request with a suitable Authorization header field
(Section 10.2). If the request already included Authorization
credentials, then the 401 response indicates that authorization has
been refused for those credentials. If the 401 response contains
the same challenge as the prior response, and the user agent has
already attempted authentication at least once, then the user
should be presented the entity that was given in the response,
since that entity may include relevant diagnostic information. HTTP
access authentication is explained in Section 11.
"""
def __init__( self, code = 401, reason = 'Unauthorized' ):
ClientError.__init__( self, code, reason )
class Forbidden( ClientError ):
"""
The server understood the request, but is refusing to fulfill it.
Authorization will not help and the request should not be repeated.
If the request method was not HEAD and the server wishes to make
public why the request has not been fulfilled, it should describe
the reason for the refusal in the entity body. This status code is
commonly used when the server does not wish to reveal exactly why
the request has been refused, or when no other response is
applicable.
"""
def __init__( self, code = 403, reason = 'Forbidden' ):
ClientError.__init__( self, code, reason )
class NotFound( ClientError ):
"""
The server has not found anything matching the Request-URI. No
indication is given of whether the condition is temporary or
permanent. If the server does not wish to make this information
available to the client, the status code 403 (forbidden) can be
used instead.
"""
def __init__( self, code = 404, reason = 'Not Found' ):
ClientError.__init__( self, code, reason )
class ServerError( HTTPStatusCode ):
"""
Response status codes beginning with the digit "5" indicate cases in
which the server is aware that it has erred or is incapable of
performing the request. If the client has not completed the request
when a 5xx code is received, it should immediately cease sending data
to the server. Except when responding to a HEAD request, the server
should include an entity containing an explanation of the error
situation, and whether it is a temporary or permanent condition.
These response codes are applicable to any request method and there
are no required header fields.
"""
pass
class InternalServerError( ServerError ):
"""
The server encountered an unexpected condition which prevented it
from fulfilling the request.
"""
def __init__( self, code = 500, reason = 'Internal Server Error' ):
ServerError.__init__( self, code, reason )
class NotImplemented( ServerError ):
"""
The server does not support the functionality required to fulfill
the request. This is the appropriate response when the server does
not recognize the request method and is not capable of supporting
it for any resource.
"""
def __init__( self, code = 501, reason = 'Not Implemented' ):
ServerError.__init__( self, code, reason )
class BadGateway( ServerError ):
"""
The server, while acting as a gateway or proxy, received an invalid
response from the upstream server it accessed in attempting to
fulfill the request.
"""
def __init__( self, code = 502, reason = 'Bad Gateway' ):
ServerError.__init__( self, code, reason )
class ServiceUnavailable( ServerError ):
"""
The server is currently unable to handle the request due to a
temporary overloading or maintenance of the server. The implication
is that this is a temporary condition which will be alleviated
after some delay.
Note: The existence of the 503 status code does not imply
that a server must use it when becoming overloaded. Some
servers may wish to simply refuse the connection.
"""
def __init__( self, code = 503, reason = 'Service Unavailable' ):
ServerError.__init__( self, code, reason )
class server( object ):
"""
An application program that accepts connections in order to service requests by sending back
responses.
"""
import logging
def __init__( self ):
"""
Initialize the internal server(s)
"""
self.__servers = list()
self.__listen = (('localhost', 8888),)
import SocketServer
for pair in self.__listen:
dbg( 'Creating server on '+str(pair) )
self.__servers.append( SocketServer.TCPServer( pair, ThreadedTCPHandler ) )
def start( self ):
"""
Start up the servers
"""
try:
for s in self.__servers:
s.serve_forever()
except KeyboardInterrupt:
pass
@staticmethod
def handle( method, uri, version, headers, body ):
"""
Handle a request after it has been processed to extract out the information within
"""
dbg( 'method={} uri={} version={} headers={} body={}'.\
format(method, str(uri), version, str(headers), body))
response = [OK()]
import os, os.path, re
req = os.path.realpath(os.path.normcase(os.path.normpath(os.getcwd()+uri['path'])))
if re.match(re.escape(os.getcwd()), req) is not None:
if os.path.isdir( req ):
if os.path.isfile(os.path.normcase(os.path.normpath(req + os.sep + 'index.html'))):
req = os.path.normcase(os.path.normpath(req + os.sep + 'index.html'))
response.extend(server.fileResponse(req))
else:
response.extend(server.directoryListing(os.getcwd(),req))
elif os.path.isfile( req ):
response.extend(server.fileResponse(req))
else:
response = [NotFound()]
else:
response = [NotFound()]
dbg(response)
return response
@staticmethod
def fileResponse( filename ):
"""
The response is a file, so attempt to guess the mime-type of the file and load the file into
the output buffer.
"""
t = server.mimetype( filename )
response = list()
response.append(header('Content-type',t))
response.append('')
response.append(server.getContents( filename ))
return response
@staticmethod
def mimetype( filename ):
"""
Attempt to guess the mime-type of a file
"""
import mimetypes
mimetypes.init()
t,e = mimetypes.guess_type( filename )
# if the mime system doesn't know, let's assume it's a plain text file.
if t is None:
dbg( filename, 'was typed as None.' )
t = 'text/plain'
return t
@staticmethod
def getContents( filename ):
"""
Get the contents of a file for output
"""
with open( filename, 'rb' ) as f:
out = ''.join(f.readlines())
return out
return None
@staticmethod
def directoryListing( cwd, d ):
"""
Generate a directory listing (in html)
"""
import os
# remove any trailing slashes
cwd = cwd.rstrip(os.sep)
# get a listing of the directory's contents
l = os.listdir( d )
# ensure there is only one trailing slash on the directory being listed
d = d[len(cwd):].rstrip(os.sep) + os.sep
# generate the headers necessary
o = [header('Content-Type','text/html'),'',]
# generate the HTML for the listing
o.append('<html>')
o.append('\t<head><title>Directory listing for {}</title></head>'.format(d))
o.append('\t<body>')
o.append('\t\t<h1>Directory listing for {}</h1>'.format(d))
o.append('\t\t<ol>')
for f in l:
if os.path.isdir(cwd+d+f):
f += os.sep
f = f.replace(os.sep,'/')
o.append('\t\t\t<li><a href="{0}{1}">{1}</a></li>'.format(d.replace(os.sep,'/'),f))
o.append('\t\t</ol>')
o.append('\t</body>')
o.append('</html>')
# remove this when development is complete
dbg( o )
return o
def __str__( self ):
"""
Generate a string representation of the class.
"""
return 'server instance.'
def SetupLogging( date = None, fmt = None, fn = None, fm = None, lvl = None ):
"""
setup the logging system for the server. see the logging module for specific details.
"""
import logging
import sys
if date is None:
date = '%Y-%m-%d %H:%M:%S'
if fmt is None:
fmt = '[%(asctime)s] %(name)s: %(levelname)s - %(message)s'
if fn is None:
fn = 'http.log'
if fm is None:
fm = 'a'
if lvl is None:
lvl = logging.DEBUG
logging.basicConfig( datefmt = date, format = fmt, filename = fn, filemode = fm, level = lvl )
def dbg( *args, **kwargs ):
"""
generate a debug message
@param args
@param kwargs
"""
a = ' '.join(map(str,args))
b = list()
for i in kwargs.items():
b.append('='.join(map(str,i)))
a += ' '.join(b)
import logging
logging.debug(a)
if __name__ == '__main__':
"""
This segment of code is only called when the file is run as a script, not a module
"""
SetupLogging()
svr = server()
svr.start()
# eof