Backport the HTTP server code from python 2.6 to 2.5
bzr revid: p_christ@hol.gr-20090910000241-3o1o5hcll10efa2y
This commit is contained in:
parent
be011d4302
commit
210196117c
|
@ -0,0 +1,587 @@
|
||||||
|
"""HTTP server base class.
|
||||||
|
|
||||||
|
Note: the class in this module doesn't implement any HTTP request; see
|
||||||
|
SimpleHTTPServer for simple implementations of GET, HEAD and POST
|
||||||
|
(including CGI scripts). It does, however, optionally implement HTTP/1.1
|
||||||
|
persistent connections, as of version 0.3.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
- BaseHTTPRequestHandler: HTTP request handler base class
|
||||||
|
- test: test function
|
||||||
|
|
||||||
|
XXX To do:
|
||||||
|
|
||||||
|
- log requests even later (to capture byte count)
|
||||||
|
- log user-agent header and other interesting goodies
|
||||||
|
- send error log to separate file
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# See also:
|
||||||
|
#
|
||||||
|
# HTTP Working Group T. Berners-Lee
|
||||||
|
# INTERNET-DRAFT R. T. Fielding
|
||||||
|
# <draft-ietf-http-v10-spec-00.txt> H. Frystyk Nielsen
|
||||||
|
# Expires September 8, 1995 March 8, 1995
|
||||||
|
#
|
||||||
|
# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
|
||||||
|
#
|
||||||
|
# and
|
||||||
|
#
|
||||||
|
# Network Working Group R. Fielding
|
||||||
|
# Request for Comments: 2616 et al
|
||||||
|
# Obsoletes: 2068 June 1999
|
||||||
|
# Category: Standards Track
|
||||||
|
#
|
||||||
|
# URL: http://www.faqs.org/rfcs/rfc2616.html
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
# ---------
|
||||||
|
#
|
||||||
|
# Here's a quote from the NCSA httpd docs about log file format.
|
||||||
|
#
|
||||||
|
# | The logfile format is as follows. Each line consists of:
|
||||||
|
# |
|
||||||
|
# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb
|
||||||
|
# |
|
||||||
|
# | host: Either the DNS name or the IP number of the remote client
|
||||||
|
# | rfc931: Any information returned by identd for this person,
|
||||||
|
# | - otherwise.
|
||||||
|
# | authuser: If user sent a userid for authentication, the user name,
|
||||||
|
# | - otherwise.
|
||||||
|
# | DD: Day
|
||||||
|
# | Mon: Month (calendar name)
|
||||||
|
# | YYYY: Year
|
||||||
|
# | hh: hour (24-hour format, the machine's timezone)
|
||||||
|
# | mm: minutes
|
||||||
|
# | ss: seconds
|
||||||
|
# | request: The first line of the HTTP request as sent by the client.
|
||||||
|
# | ddd: the status code returned by the server, - if not available.
|
||||||
|
# | bbbb: the total number of bytes sent,
|
||||||
|
# | *not including the HTTP/1.0 header*, - if not available
|
||||||
|
# |
|
||||||
|
# | You can determine the name of the file accessed through request.
|
||||||
|
#
|
||||||
|
# (Actually, the latter is only true if you know the server configuration
|
||||||
|
# at the time the request was made!)
|
||||||
|
|
||||||
|
__version__ = "0.3"
|
||||||
|
|
||||||
|
__all__ = ["HTTPServer", "BaseHTTPRequestHandler"]
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import socket # For gethostbyaddr()
|
||||||
|
import mimetools
|
||||||
|
import SocketServer
|
||||||
|
|
||||||
|
# Default error message template
|
||||||
|
DEFAULT_ERROR_MESSAGE = """\
|
||||||
|
<head>
|
||||||
|
<title>Error response</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Error response</h1>
|
||||||
|
<p>Error code %(code)d.
|
||||||
|
<p>Message: %(message)s.
|
||||||
|
<p>Error code explanation: %(code)s = %(explain)s.
|
||||||
|
</body>
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
|
||||||
|
|
||||||
|
def _quote_html(html):
|
||||||
|
return html.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
class HTTPServer(SocketServer.TCPServer):
|
||||||
|
|
||||||
|
allow_reuse_address = 1 # Seems to make sense in testing environment
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
"""Override server_bind to store the server name."""
|
||||||
|
SocketServer.TCPServer.server_bind(self)
|
||||||
|
host, port = self.socket.getsockname()[:2]
|
||||||
|
self.server_name = socket.getfqdn(host)
|
||||||
|
self.server_port = port
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler):
|
||||||
|
|
||||||
|
"""HTTP request handler base class.
|
||||||
|
|
||||||
|
The following explanation of HTTP serves to guide you through the
|
||||||
|
code as well as to expose any misunderstandings I may have about
|
||||||
|
HTTP (so you don't need to read the code to figure out I'm wrong
|
||||||
|
:-).
|
||||||
|
|
||||||
|
HTTP (HyperText Transfer Protocol) is an extensible protocol on
|
||||||
|
top of a reliable stream transport (e.g. TCP/IP). The protocol
|
||||||
|
recognizes three parts to a request:
|
||||||
|
|
||||||
|
1. One line identifying the request type and path
|
||||||
|
2. An optional set of RFC-822-style headers
|
||||||
|
3. An optional data part
|
||||||
|
|
||||||
|
The headers and data are separated by a blank line.
|
||||||
|
|
||||||
|
The first line of the request has the form
|
||||||
|
|
||||||
|
<command> <path> <version>
|
||||||
|
|
||||||
|
where <command> is a (case-sensitive) keyword such as GET or POST,
|
||||||
|
<path> is a string containing path information for the request,
|
||||||
|
and <version> should be the string "HTTP/1.0" or "HTTP/1.1".
|
||||||
|
<path> is encoded using the URL encoding scheme (using %xx to signify
|
||||||
|
the ASCII character with hex code xx).
|
||||||
|
|
||||||
|
The specification specifies that lines are separated by CRLF but
|
||||||
|
for compatibility with the widest range of clients recommends
|
||||||
|
servers also handle LF. Similarly, whitespace in the request line
|
||||||
|
is treated sensibly (allowing multiple spaces between components
|
||||||
|
and allowing trailing whitespace).
|
||||||
|
|
||||||
|
Similarly, for output, lines ought to be separated by CRLF pairs
|
||||||
|
but most clients grok LF characters just fine.
|
||||||
|
|
||||||
|
If the first line of the request has the form
|
||||||
|
|
||||||
|
<command> <path>
|
||||||
|
|
||||||
|
(i.e. <version> is left out) then this is assumed to be an HTTP
|
||||||
|
0.9 request; this form has no optional headers and data part and
|
||||||
|
the reply consists of just the data.
|
||||||
|
|
||||||
|
The reply form of the HTTP 1.x protocol again has three parts:
|
||||||
|
|
||||||
|
1. One line giving the response code
|
||||||
|
2. An optional set of RFC-822-style headers
|
||||||
|
3. The data
|
||||||
|
|
||||||
|
Again, the headers and data are separated by a blank line.
|
||||||
|
|
||||||
|
The response code line has the form
|
||||||
|
|
||||||
|
<version> <responsecode> <responsestring>
|
||||||
|
|
||||||
|
where <version> is the protocol version ("HTTP/1.0" or "HTTP/1.1"),
|
||||||
|
<responsecode> is a 3-digit response code indicating success or
|
||||||
|
failure of the request, and <responsestring> is an optional
|
||||||
|
human-readable string explaining what the response code means.
|
||||||
|
|
||||||
|
This server parses the request and the headers, and then calls a
|
||||||
|
function specific to the request type (<command>). Specifically,
|
||||||
|
a request SPAM will be handled by a method do_SPAM(). If no
|
||||||
|
such method exists the server sends an error response to the
|
||||||
|
client. If it exists, it is called with no arguments:
|
||||||
|
|
||||||
|
do_SPAM()
|
||||||
|
|
||||||
|
Note that the request name is case sensitive (i.e. SPAM and spam
|
||||||
|
are different requests).
|
||||||
|
|
||||||
|
The various request details are stored in instance variables:
|
||||||
|
|
||||||
|
- client_address is the client IP address in the form (host,
|
||||||
|
port);
|
||||||
|
|
||||||
|
- command, path and version are the broken-down request line;
|
||||||
|
|
||||||
|
- headers is an instance of mimetools.Message (or a derived
|
||||||
|
class) containing the header information;
|
||||||
|
|
||||||
|
- rfile is a file object open for reading positioned at the
|
||||||
|
start of the optional input data part;
|
||||||
|
|
||||||
|
- wfile is a file object open for writing.
|
||||||
|
|
||||||
|
IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING!
|
||||||
|
|
||||||
|
The first thing to be written must be the response line. Then
|
||||||
|
follow 0 or more header lines, then a blank line, and then the
|
||||||
|
actual data (if any). The meaning of the header lines depends on
|
||||||
|
the command executed by the server; in most cases, when data is
|
||||||
|
returned, there should be at least one header line of the form
|
||||||
|
|
||||||
|
Content-type: <type>/<subtype>
|
||||||
|
|
||||||
|
where <type> and <subtype> should be registered MIME types,
|
||||||
|
e.g. "text/html" or "text/plain".
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The Python system version, truncated to its first component.
|
||||||
|
sys_version = "Python/" + sys.version.split()[0]
|
||||||
|
|
||||||
|
# The server software version. You may want to override this.
|
||||||
|
# The format is multiple whitespace-separated strings,
|
||||||
|
# where each string is of the form name[/version].
|
||||||
|
server_version = "BaseHTTP/" + __version__
|
||||||
|
|
||||||
|
# The default request version. This only affects responses up until
|
||||||
|
# the point where the request line is parsed, so it mainly decides what
|
||||||
|
# the client gets back when sending a malformed request line.
|
||||||
|
# Most web servers default to HTTP 0.9, i.e. don't send a status line.
|
||||||
|
default_request_version = "HTTP/0.9"
|
||||||
|
|
||||||
|
def parse_request(self):
|
||||||
|
"""Parse a request (internal).
|
||||||
|
|
||||||
|
The request should be stored in self.raw_requestline; the results
|
||||||
|
are in self.command, self.path, self.request_version and
|
||||||
|
self.headers.
|
||||||
|
|
||||||
|
Return True for success, False for failure; on failure, an
|
||||||
|
error is sent back.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.command = None # set in case of error on the first line
|
||||||
|
self.request_version = version = self.default_request_version
|
||||||
|
self.close_connection = 1
|
||||||
|
requestline = self.raw_requestline
|
||||||
|
if requestline[-2:] == '\r\n':
|
||||||
|
requestline = requestline[:-2]
|
||||||
|
elif requestline[-1:] == '\n':
|
||||||
|
requestline = requestline[:-1]
|
||||||
|
self.requestline = requestline
|
||||||
|
words = requestline.split()
|
||||||
|
if len(words) == 3:
|
||||||
|
[command, path, version] = words
|
||||||
|
if version[:5] != 'HTTP/':
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
base_version_number = version.split('/', 1)[1]
|
||||||
|
version_number = base_version_number.split(".")
|
||||||
|
# RFC 2145 section 3.1 says there can be only one "." and
|
||||||
|
# - major and minor numbers MUST be treated as
|
||||||
|
# separate integers;
|
||||||
|
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
|
||||||
|
# turn is lower than HTTP/12.3;
|
||||||
|
# - Leading zeros MUST be ignored by recipients.
|
||||||
|
if len(version_number) != 2:
|
||||||
|
raise ValueError
|
||||||
|
version_number = int(version_number[0]), int(version_number[1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
|
||||||
|
self.close_connection = 0
|
||||||
|
if version_number >= (2, 0):
|
||||||
|
self.send_error(505,
|
||||||
|
"Invalid HTTP Version (%s)" % base_version_number)
|
||||||
|
return False
|
||||||
|
elif len(words) == 2:
|
||||||
|
[command, path] = words
|
||||||
|
self.close_connection = 1
|
||||||
|
if command != 'GET':
|
||||||
|
self.send_error(400,
|
||||||
|
"Bad HTTP/0.9 request type (%r)" % command)
|
||||||
|
return False
|
||||||
|
elif not words:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.send_error(400, "Bad request syntax (%r)" % requestline)
|
||||||
|
return False
|
||||||
|
self.command, self.path, self.request_version = command, path, version
|
||||||
|
|
||||||
|
# Examine the headers and look for a Connection directive
|
||||||
|
self.headers = self.MessageClass(self.rfile, 0)
|
||||||
|
|
||||||
|
conntype = self.headers.get('Connection', "")
|
||||||
|
if conntype.lower() == 'close':
|
||||||
|
self.close_connection = 1
|
||||||
|
elif (conntype.lower() == 'keep-alive' and
|
||||||
|
self.protocol_version >= "HTTP/1.1"):
|
||||||
|
self.close_connection = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_one_request(self):
|
||||||
|
"""Handle a single HTTP request.
|
||||||
|
|
||||||
|
You normally don't need to override this method; see the class
|
||||||
|
__doc__ string for information on how to handle specific HTTP
|
||||||
|
commands such as GET and POST.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.raw_requestline = self.rfile.readline()
|
||||||
|
if not self.raw_requestline:
|
||||||
|
self.close_connection = 1
|
||||||
|
return
|
||||||
|
if not self.parse_request(): # An error code has been sent, just exit
|
||||||
|
return
|
||||||
|
mname = 'do_' + self.command
|
||||||
|
if not hasattr(self, mname):
|
||||||
|
self.send_error(501, "Unsupported method (%r)" % self.command)
|
||||||
|
return
|
||||||
|
method = getattr(self, mname)
|
||||||
|
method()
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
"""Handle multiple requests if necessary."""
|
||||||
|
self.close_connection = 1
|
||||||
|
|
||||||
|
self.handle_one_request()
|
||||||
|
while not self.close_connection:
|
||||||
|
self.handle_one_request()
|
||||||
|
|
||||||
|
def send_error(self, code, message=None):
|
||||||
|
"""Send and log an error reply.
|
||||||
|
|
||||||
|
Arguments are the error code, and a detailed message.
|
||||||
|
The detailed message defaults to the short entry matching the
|
||||||
|
response code.
|
||||||
|
|
||||||
|
This sends an error response (so it must be called before any
|
||||||
|
output has been generated), logs the error, and finally sends
|
||||||
|
a piece of HTML explaining the error to the user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
short, long = self.responses[code]
|
||||||
|
except KeyError:
|
||||||
|
short, long = '???', '???'
|
||||||
|
if message is None:
|
||||||
|
message = short
|
||||||
|
explain = long
|
||||||
|
self.log_error("code %d, message %s", code, message)
|
||||||
|
# using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
|
||||||
|
content = (self.error_message_format %
|
||||||
|
{'code': code, 'message': _quote_html(message), 'explain': explain})
|
||||||
|
self.send_response(code, message)
|
||||||
|
self.send_header("Content-Type", self.error_content_type)
|
||||||
|
self.send_header('Connection', 'close')
|
||||||
|
self.end_headers()
|
||||||
|
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
error_message_format = DEFAULT_ERROR_MESSAGE
|
||||||
|
error_content_type = DEFAULT_ERROR_CONTENT_TYPE
|
||||||
|
|
||||||
|
def send_response(self, code, message=None):
|
||||||
|
"""Send the response header and log the response code.
|
||||||
|
|
||||||
|
Also send two standard headers with the server software
|
||||||
|
version and the current date.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.log_request(code)
|
||||||
|
if message is None:
|
||||||
|
if code in self.responses:
|
||||||
|
message = self.responses[code][0]
|
||||||
|
else:
|
||||||
|
message = ''
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("%s %d %s\r\n" %
|
||||||
|
(self.protocol_version, code, message))
|
||||||
|
# print (self.protocol_version, code, message)
|
||||||
|
self.send_header('Server', self.version_string())
|
||||||
|
self.send_header('Date', self.date_time_string())
|
||||||
|
|
||||||
|
def send_header(self, keyword, value):
|
||||||
|
"""Send a MIME header."""
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("%s: %s\r\n" % (keyword, value))
|
||||||
|
|
||||||
|
if keyword.lower() == 'connection':
|
||||||
|
if value.lower() == 'close':
|
||||||
|
self.close_connection = 1
|
||||||
|
elif value.lower() == 'keep-alive':
|
||||||
|
self.close_connection = 0
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
"""Send the blank line ending the MIME headers."""
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("\r\n")
|
||||||
|
|
||||||
|
def log_request(self, code='-', size='-'):
|
||||||
|
"""Log an accepted request.
|
||||||
|
|
||||||
|
This is called by send_response().
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.log_message('"%s" %s %s',
|
||||||
|
self.requestline, str(code), str(size))
|
||||||
|
|
||||||
|
def log_error(self, format, *args):
|
||||||
|
"""Log an error.
|
||||||
|
|
||||||
|
This is called when a request cannot be fulfilled. By
|
||||||
|
default it passes the message on to log_message().
|
||||||
|
|
||||||
|
Arguments are the same as for log_message().
|
||||||
|
|
||||||
|
XXX This should go to the separate error log.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.log_message(format, *args)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""Log an arbitrary message.
|
||||||
|
|
||||||
|
This is used by all other logging functions. Override
|
||||||
|
it if you have specific logging wishes.
|
||||||
|
|
||||||
|
The first argument, FORMAT, is a format string for the
|
||||||
|
message to be logged. If the format string contains
|
||||||
|
any % escapes requiring parameters, they should be
|
||||||
|
specified as subsequent arguments (it's just like
|
||||||
|
printf!).
|
||||||
|
|
||||||
|
The client host and current date/time are prefixed to
|
||||||
|
every message.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
sys.stderr.write("%s - - [%s] %s\n" %
|
||||||
|
(self.address_string(),
|
||||||
|
self.log_date_time_string(),
|
||||||
|
format%args))
|
||||||
|
|
||||||
|
def version_string(self):
|
||||||
|
"""Return the server software version string."""
|
||||||
|
return self.server_version + ' ' + self.sys_version
|
||||||
|
|
||||||
|
def date_time_string(self, timestamp=None):
|
||||||
|
"""Return the current date and time formatted for a message header."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = time.time()
|
||||||
|
year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
|
||||||
|
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
|
||||||
|
self.weekdayname[wd],
|
||||||
|
day, self.monthname[month], year,
|
||||||
|
hh, mm, ss)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def log_date_time_string(self):
|
||||||
|
"""Return the current time formatted for logging."""
|
||||||
|
now = time.time()
|
||||||
|
year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
|
||||||
|
s = "%02d/%3s/%04d %02d:%02d:%02d" % (
|
||||||
|
day, self.monthname[month], year, hh, mm, ss)
|
||||||
|
return s
|
||||||
|
|
||||||
|
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||||
|
|
||||||
|
monthname = [None,
|
||||||
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||||
|
|
||||||
|
def address_string(self):
|
||||||
|
"""Return the client address formatted for logging.
|
||||||
|
|
||||||
|
This version looks up the full hostname using gethostbyaddr(),
|
||||||
|
and tries to find a name that contains at least one dot.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
host, port = self.client_address[:2]
|
||||||
|
return socket.getfqdn(host)
|
||||||
|
|
||||||
|
# Essentially static class variables
|
||||||
|
|
||||||
|
# The version of the HTTP protocol we support.
|
||||||
|
# Set this to HTTP/1.1 to enable automatic keepalive
|
||||||
|
protocol_version = "HTTP/1.0"
|
||||||
|
|
||||||
|
# The Message-like class used to parse headers
|
||||||
|
MessageClass = mimetools.Message
|
||||||
|
|
||||||
|
# Table mapping response codes to messages; entries have the
|
||||||
|
# form {code: (shortmessage, longmessage)}.
|
||||||
|
# See RFC 2616.
|
||||||
|
responses = {
|
||||||
|
100: ('Continue', 'Request received, please continue'),
|
||||||
|
101: ('Switching Protocols',
|
||||||
|
'Switching to new protocol; obey Upgrade header'),
|
||||||
|
|
||||||
|
200: ('OK', 'Request fulfilled, document follows'),
|
||||||
|
201: ('Created', 'Document created, URL follows'),
|
||||||
|
202: ('Accepted',
|
||||||
|
'Request accepted, processing continues off-line'),
|
||||||
|
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
|
||||||
|
204: ('No Content', 'Request fulfilled, nothing follows'),
|
||||||
|
205: ('Reset Content', 'Clear input form for further input.'),
|
||||||
|
206: ('Partial Content', 'Partial content follows.'),
|
||||||
|
|
||||||
|
300: ('Multiple Choices',
|
||||||
|
'Object has several resources -- see URI list'),
|
||||||
|
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
|
||||||
|
302: ('Found', 'Object moved temporarily -- see URI list'),
|
||||||
|
303: ('See Other', 'Object moved -- see Method and URL list'),
|
||||||
|
304: ('Not Modified',
|
||||||
|
'Document has not changed since given time'),
|
||||||
|
305: ('Use Proxy',
|
||||||
|
'You must use proxy specified in Location to access this '
|
||||||
|
'resource.'),
|
||||||
|
307: ('Temporary Redirect',
|
||||||
|
'Object moved temporarily -- see URI list'),
|
||||||
|
|
||||||
|
400: ('Bad Request',
|
||||||
|
'Bad request syntax or unsupported method'),
|
||||||
|
401: ('Unauthorized',
|
||||||
|
'No permission -- see authorization schemes'),
|
||||||
|
402: ('Payment Required',
|
||||||
|
'No payment -- see charging schemes'),
|
||||||
|
403: ('Forbidden',
|
||||||
|
'Request forbidden -- authorization will not help'),
|
||||||
|
404: ('Not Found', 'Nothing matches the given URI'),
|
||||||
|
405: ('Method Not Allowed',
|
||||||
|
'Specified method is invalid for this server.'),
|
||||||
|
406: ('Not Acceptable', 'URI not available in preferred format.'),
|
||||||
|
407: ('Proxy Authentication Required', 'You must authenticate with '
|
||||||
|
'this proxy before proceeding.'),
|
||||||
|
408: ('Request Timeout', 'Request timed out; try again later.'),
|
||||||
|
409: ('Conflict', 'Request conflict.'),
|
||||||
|
410: ('Gone',
|
||||||
|
'URI no longer exists and has been permanently removed.'),
|
||||||
|
411: ('Length Required', 'Client must specify Content-Length.'),
|
||||||
|
412: ('Precondition Failed', 'Precondition in headers is false.'),
|
||||||
|
413: ('Request Entity Too Large', 'Entity is too large.'),
|
||||||
|
414: ('Request-URI Too Long', 'URI is too long.'),
|
||||||
|
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
|
||||||
|
416: ('Requested Range Not Satisfiable',
|
||||||
|
'Cannot satisfy request range.'),
|
||||||
|
417: ('Expectation Failed',
|
||||||
|
'Expect condition could not be satisfied.'),
|
||||||
|
|
||||||
|
500: ('Internal Server Error', 'Server got itself in trouble'),
|
||||||
|
501: ('Not Implemented',
|
||||||
|
'Server does not support this operation'),
|
||||||
|
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
|
||||||
|
503: ('Service Unavailable',
|
||||||
|
'The server cannot process the request due to a high load'),
|
||||||
|
504: ('Gateway Timeout',
|
||||||
|
'The gateway server did not receive a timely response'),
|
||||||
|
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test(HandlerClass = BaseHTTPRequestHandler,
|
||||||
|
ServerClass = HTTPServer, protocol="HTTP/1.0"):
|
||||||
|
"""Test the HTTP request handler class.
|
||||||
|
|
||||||
|
This runs an HTTP server on port 8000 (or the first command line
|
||||||
|
argument).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if sys.argv[1:]:
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
else:
|
||||||
|
port = 8000
|
||||||
|
server_address = ('', port)
|
||||||
|
|
||||||
|
HandlerClass.protocol_version = protocol
|
||||||
|
httpd = ServerClass(server_address, HandlerClass)
|
||||||
|
|
||||||
|
sa = httpd.socket.getsockname()
|
||||||
|
print "Serving HTTP on", sa[0], "port", sa[1], "..."
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
|
@ -0,0 +1,611 @@
|
||||||
|
"""Simple XML-RPC Server.
|
||||||
|
|
||||||
|
This module can be used to create simple XML-RPC servers
|
||||||
|
by creating a server and either installing functions, a
|
||||||
|
class instance, or by extending the SimpleXMLRPCServer
|
||||||
|
class.
|
||||||
|
|
||||||
|
It can also be used to handle XML-RPC requests in a CGI
|
||||||
|
environment using CGIXMLRPCRequestHandler.
|
||||||
|
|
||||||
|
A list of possible usage patterns follows:
|
||||||
|
|
||||||
|
1. Install functions:
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_function(pow)
|
||||||
|
server.register_function(lambda x,y: x+y, 'add')
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
2. Install an instance:
|
||||||
|
|
||||||
|
class MyFuncs:
|
||||||
|
def __init__(self):
|
||||||
|
# make all of the string functions available through
|
||||||
|
# string.func_name
|
||||||
|
import string
|
||||||
|
self.string = string
|
||||||
|
def _listMethods(self):
|
||||||
|
# implement this method so that system.listMethods
|
||||||
|
# knows to advertise the strings methods
|
||||||
|
return list_public_methods(self) + \
|
||||||
|
['string.' + method for method in list_public_methods(self.string)]
|
||||||
|
def pow(self, x, y): return pow(x, y)
|
||||||
|
def add(self, x, y) : return x + y
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_introspection_functions()
|
||||||
|
server.register_instance(MyFuncs())
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
3. Install an instance with custom dispatch method:
|
||||||
|
|
||||||
|
class Math:
|
||||||
|
def _listMethods(self):
|
||||||
|
# this method must be present for system.listMethods
|
||||||
|
# to work
|
||||||
|
return ['add', 'pow']
|
||||||
|
def _methodHelp(self, method):
|
||||||
|
# this method must be present for system.methodHelp
|
||||||
|
# to work
|
||||||
|
if method == 'add':
|
||||||
|
return "add(2,3) => 5"
|
||||||
|
elif method == 'pow':
|
||||||
|
return "pow(x, y[, z]) => number"
|
||||||
|
else:
|
||||||
|
# By convention, return empty
|
||||||
|
# string if no help is available
|
||||||
|
return ""
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
if method == 'pow':
|
||||||
|
return pow(*params)
|
||||||
|
elif method == 'add':
|
||||||
|
return params[0] + params[1]
|
||||||
|
else:
|
||||||
|
raise 'bad method'
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_introspection_functions()
|
||||||
|
server.register_instance(Math())
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
4. Subclass SimpleXMLRPCServer:
|
||||||
|
|
||||||
|
class MathServer(SimpleXMLRPCServer):
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
try:
|
||||||
|
# We are forcing the 'export_' prefix on methods that are
|
||||||
|
# callable through XML-RPC to prevent potential security
|
||||||
|
# problems
|
||||||
|
func = getattr(self, 'export_' + method)
|
||||||
|
except AttributeError:
|
||||||
|
raise Exception('method "%s" is not supported' % method)
|
||||||
|
else:
|
||||||
|
return func(*params)
|
||||||
|
|
||||||
|
def export_add(self, x, y):
|
||||||
|
return x + y
|
||||||
|
|
||||||
|
server = MathServer(("localhost", 8000))
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
5. CGI script:
|
||||||
|
|
||||||
|
server = CGIXMLRPCRequestHandler()
|
||||||
|
server.register_function(pow)
|
||||||
|
server.handle_request()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Written by Brian Quinlan (brian@sweetapp.com).
|
||||||
|
# Based on code written by Fredrik Lundh.
|
||||||
|
|
||||||
|
import xmlrpclib
|
||||||
|
from xmlrpclib import Fault
|
||||||
|
import SocketServer
|
||||||
|
import BaseHTTPServer
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
except ImportError:
|
||||||
|
fcntl = None
|
||||||
|
|
||||||
|
def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
|
||||||
|
"""resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
|
||||||
|
|
||||||
|
Resolves a dotted attribute name to an object. Raises
|
||||||
|
an AttributeError if any attribute in the chain starts with a '_'.
|
||||||
|
|
||||||
|
If the optional allow_dotted_names argument is false, dots are not
|
||||||
|
supported and this function operates similar to getattr(obj, attr).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if allow_dotted_names:
|
||||||
|
attrs = attr.split('.')
|
||||||
|
else:
|
||||||
|
attrs = [attr]
|
||||||
|
|
||||||
|
for i in attrs:
|
||||||
|
if i.startswith('_'):
|
||||||
|
raise AttributeError(
|
||||||
|
'attempt to access private attribute "%s"' % i
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
obj = getattr(obj,i)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def list_public_methods(obj):
|
||||||
|
"""Returns a list of attribute strings, found in the specified
|
||||||
|
object, which represent callable attributes"""
|
||||||
|
|
||||||
|
return [member for member in dir(obj)
|
||||||
|
if not member.startswith('_') and
|
||||||
|
hasattr(getattr(obj, member), '__call__')]
|
||||||
|
|
||||||
|
def remove_duplicates(lst):
|
||||||
|
"""remove_duplicates([2,2,2,1,3,3]) => [3,1,2]
|
||||||
|
|
||||||
|
Returns a copy of a list without duplicates. Every list
|
||||||
|
item must be hashable and the order of the items in the
|
||||||
|
resulting list is not defined.
|
||||||
|
"""
|
||||||
|
u = {}
|
||||||
|
for x in lst:
|
||||||
|
u[x] = 1
|
||||||
|
|
||||||
|
return u.keys()
|
||||||
|
|
||||||
|
class SimpleXMLRPCDispatcher:
|
||||||
|
"""Mix-in class that dispatches XML-RPC requests.
|
||||||
|
|
||||||
|
This class is used to register XML-RPC method handlers
|
||||||
|
and then to dispatch them. There should never be any
|
||||||
|
reason to instantiate this class directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, allow_none, encoding):
|
||||||
|
self.funcs = {}
|
||||||
|
self.instance = None
|
||||||
|
self.allow_none = allow_none
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def register_instance(self, instance, allow_dotted_names=False):
|
||||||
|
"""Registers an instance to respond to XML-RPC requests.
|
||||||
|
|
||||||
|
Only one instance can be installed at a time.
|
||||||
|
|
||||||
|
If the registered instance has a _dispatch method then that
|
||||||
|
method will be called with the name of the XML-RPC method and
|
||||||
|
its parameters as a tuple
|
||||||
|
e.g. instance._dispatch('add',(2,3))
|
||||||
|
|
||||||
|
If the registered instance does not have a _dispatch method
|
||||||
|
then the instance will be searched to find a matching method
|
||||||
|
and, if found, will be called. Methods beginning with an '_'
|
||||||
|
are considered private and will not be called by
|
||||||
|
SimpleXMLRPCServer.
|
||||||
|
|
||||||
|
If a registered function matches a XML-RPC request, then it
|
||||||
|
will be called instead of the registered instance.
|
||||||
|
|
||||||
|
If the optional allow_dotted_names argument is true and the
|
||||||
|
instance does not have a _dispatch method, method names
|
||||||
|
containing dots are supported and resolved, as long as none of
|
||||||
|
the name segments start with an '_'.
|
||||||
|
|
||||||
|
*** SECURITY WARNING: ***
|
||||||
|
|
||||||
|
Enabling the allow_dotted_names options allows intruders
|
||||||
|
to access your module's global variables and may allow
|
||||||
|
intruders to execute arbitrary code on your machine. Only
|
||||||
|
use this option on a secure, closed network.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.instance = instance
|
||||||
|
self.allow_dotted_names = allow_dotted_names
|
||||||
|
|
||||||
|
def register_function(self, function, name = None):
|
||||||
|
"""Registers a function to respond to XML-RPC requests.
|
||||||
|
|
||||||
|
The optional name argument can be used to set a Unicode name
|
||||||
|
for the function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
name = function.__name__
|
||||||
|
self.funcs[name] = function
|
||||||
|
|
||||||
|
def register_introspection_functions(self):
|
||||||
|
"""Registers the XML-RPC introspection methods in the system
|
||||||
|
namespace.
|
||||||
|
|
||||||
|
see http://xmlrpc.usefulinc.com/doc/reserved.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.funcs.update({'system.listMethods' : self.system_listMethods,
|
||||||
|
'system.methodSignature' : self.system_methodSignature,
|
||||||
|
'system.methodHelp' : self.system_methodHelp})
|
||||||
|
|
||||||
|
def register_multicall_functions(self):
|
||||||
|
"""Registers the XML-RPC multicall method in the system
|
||||||
|
namespace.
|
||||||
|
|
||||||
|
see http://www.xmlrpc.com/discuss/msgReader$1208"""
|
||||||
|
|
||||||
|
self.funcs.update({'system.multicall' : self.system_multicall})
|
||||||
|
|
||||||
|
def _marshaled_dispatch(self, data, dispatch_method = None):
|
||||||
|
"""Dispatches an XML-RPC method from marshalled (XML) data.
|
||||||
|
|
||||||
|
XML-RPC methods are dispatched from the marshalled (XML) data
|
||||||
|
using the _dispatch method and the result is returned as
|
||||||
|
marshalled data. For backwards compatibility, a dispatch
|
||||||
|
function can be provided as an argument (see comment in
|
||||||
|
SimpleXMLRPCRequestHandler.do_POST) but overriding the
|
||||||
|
existing method through subclassing is the prefered means
|
||||||
|
of changing method dispatch behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
params, method = xmlrpclib.loads(data)
|
||||||
|
|
||||||
|
# generate response
|
||||||
|
if dispatch_method is not None:
|
||||||
|
response = dispatch_method(method, params)
|
||||||
|
else:
|
||||||
|
response = self._dispatch(method, params)
|
||||||
|
# wrap response in a singleton tuple
|
||||||
|
response = (response,)
|
||||||
|
response = xmlrpclib.dumps(response, methodresponse=1,
|
||||||
|
allow_none=self.allow_none, encoding=self.encoding)
|
||||||
|
except Fault, fault:
|
||||||
|
response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
|
||||||
|
encoding=self.encoding)
|
||||||
|
except:
|
||||||
|
# report exception back to server
|
||||||
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||||
|
response = xmlrpclib.dumps(
|
||||||
|
xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
|
||||||
|
encoding=self.encoding, allow_none=self.allow_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def system_listMethods(self):
|
||||||
|
"""system.listMethods() => ['add', 'subtract', 'multiple']
|
||||||
|
|
||||||
|
Returns a list of the methods supported by the server."""
|
||||||
|
|
||||||
|
methods = self.funcs.keys()
|
||||||
|
if self.instance is not None:
|
||||||
|
# Instance can implement _listMethod to return a list of
|
||||||
|
# methods
|
||||||
|
if hasattr(self.instance, '_listMethods'):
|
||||||
|
methods = remove_duplicates(
|
||||||
|
methods + self.instance._listMethods()
|
||||||
|
)
|
||||||
|
# if the instance has a _dispatch method then we
|
||||||
|
# don't have enough information to provide a list
|
||||||
|
# of methods
|
||||||
|
elif not hasattr(self.instance, '_dispatch'):
|
||||||
|
methods = remove_duplicates(
|
||||||
|
methods + list_public_methods(self.instance)
|
||||||
|
)
|
||||||
|
methods.sort()
|
||||||
|
return methods
|
||||||
|
|
||||||
|
def system_methodSignature(self, method_name):
|
||||||
|
"""system.methodSignature('add') => [double, int, int]
|
||||||
|
|
||||||
|
Returns a list describing the signature of the method. In the
|
||||||
|
above example, the add method takes two integers as arguments
|
||||||
|
and returns a double result.
|
||||||
|
|
||||||
|
This server does NOT support system.methodSignature."""
|
||||||
|
|
||||||
|
# See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
|
||||||
|
|
||||||
|
return 'signatures not supported'
|
||||||
|
|
||||||
|
def system_methodHelp(self, method_name):
|
||||||
|
"""system.methodHelp('add') => "Adds two integers together"
|
||||||
|
|
||||||
|
Returns a string containing documentation for the specified method."""
|
||||||
|
|
||||||
|
method = None
|
||||||
|
if method_name in self.funcs:
|
||||||
|
method = self.funcs[method_name]
|
||||||
|
elif self.instance is not None:
|
||||||
|
# Instance can implement _methodHelp to return help for a method
|
||||||
|
if hasattr(self.instance, '_methodHelp'):
|
||||||
|
return self.instance._methodHelp(method_name)
|
||||||
|
# if the instance has a _dispatch method then we
|
||||||
|
# don't have enough information to provide help
|
||||||
|
elif not hasattr(self.instance, '_dispatch'):
|
||||||
|
try:
|
||||||
|
method = resolve_dotted_attribute(
|
||||||
|
self.instance,
|
||||||
|
method_name,
|
||||||
|
self.allow_dotted_names
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Note that we aren't checking that the method actually
|
||||||
|
# be a callable object of some kind
|
||||||
|
if method is None:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
import pydoc
|
||||||
|
return pydoc.getdoc(method)
|
||||||
|
|
||||||
|
def system_multicall(self, call_list):
|
||||||
|
"""system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
|
||||||
|
[[4], ...]
|
||||||
|
|
||||||
|
Allows the caller to package multiple XML-RPC calls into a single
|
||||||
|
request.
|
||||||
|
|
||||||
|
See http://www.xmlrpc.com/discuss/msgReader$1208
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for call in call_list:
|
||||||
|
method_name = call['methodName']
|
||||||
|
params = call['params']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# XXX A marshalling error in any response will fail the entire
|
||||||
|
# multicall. If someone cares they should fix this.
|
||||||
|
results.append([self._dispatch(method_name, params)])
|
||||||
|
except Fault, fault:
|
||||||
|
results.append(
|
||||||
|
{'faultCode' : fault.faultCode,
|
||||||
|
'faultString' : fault.faultString}
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||||
|
results.append(
|
||||||
|
{'faultCode' : 1,
|
||||||
|
'faultString' : "%s:%s" % (exc_type, exc_value)}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
"""Dispatches the XML-RPC method.
|
||||||
|
|
||||||
|
XML-RPC calls are forwarded to a registered function that
|
||||||
|
matches the called XML-RPC method name. If no such function
|
||||||
|
exists then the call is forwarded to the registered instance,
|
||||||
|
if available.
|
||||||
|
|
||||||
|
If the registered instance has a _dispatch method then that
|
||||||
|
method will be called with the name of the XML-RPC method and
|
||||||
|
its parameters as a tuple
|
||||||
|
e.g. instance._dispatch('add',(2,3))
|
||||||
|
|
||||||
|
If the registered instance does not have a _dispatch method
|
||||||
|
then the instance will be searched to find a matching method
|
||||||
|
and, if found, will be called.
|
||||||
|
|
||||||
|
Methods beginning with an '_' are considered private and will
|
||||||
|
not be called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
func = None
|
||||||
|
try:
|
||||||
|
# check to see if a matching function has been registered
|
||||||
|
func = self.funcs[method]
|
||||||
|
except KeyError:
|
||||||
|
if self.instance is not None:
|
||||||
|
# check for a _dispatch method
|
||||||
|
if hasattr(self.instance, '_dispatch'):
|
||||||
|
return self.instance._dispatch(method, params)
|
||||||
|
else:
|
||||||
|
# call instance method directly
|
||||||
|
try:
|
||||||
|
func = resolve_dotted_attribute(
|
||||||
|
self.instance,
|
||||||
|
method,
|
||||||
|
self.allow_dotted_names
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if func is not None:
|
||||||
|
return func(*params)
|
||||||
|
else:
|
||||||
|
raise Exception('method "%s" is not supported' % method)
|
||||||
|
|
||||||
|
class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
"""Simple XML-RPC request handler class.
|
||||||
|
|
||||||
|
Handles all HTTP POST requests and attempts to decode them as
|
||||||
|
XML-RPC requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Class attribute listing the accessible path components;
|
||||||
|
# paths not on this list will result in a 404 error.
|
||||||
|
rpc_paths = ('/', '/RPC2')
|
||||||
|
|
||||||
|
def is_rpc_path_valid(self):
|
||||||
|
if self.rpc_paths:
|
||||||
|
return self.path in self.rpc_paths
|
||||||
|
else:
|
||||||
|
# If .rpc_paths is empty, just assume all paths are legal
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
"""Handles the HTTP POST request.
|
||||||
|
|
||||||
|
Attempts to interpret all HTTP POST requests as XML-RPC calls,
|
||||||
|
which are forwarded to the server's _dispatch method for handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check that the path is legal
|
||||||
|
if not self.is_rpc_path_valid():
|
||||||
|
self.report_404()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get arguments by reading body of request.
|
||||||
|
# We read this in chunks to avoid straining
|
||||||
|
# socket.read(); around the 10 or 15Mb mark, some platforms
|
||||||
|
# begin to have problems (bug #792570).
|
||||||
|
max_chunk_size = 10*1024*1024
|
||||||
|
size_remaining = int(self.headers["content-length"])
|
||||||
|
L = []
|
||||||
|
while size_remaining:
|
||||||
|
chunk_size = min(size_remaining, max_chunk_size)
|
||||||
|
L.append(self.rfile.read(chunk_size))
|
||||||
|
size_remaining -= len(L[-1])
|
||||||
|
data = ''.join(L)
|
||||||
|
|
||||||
|
# In previous versions of SimpleXMLRPCServer, _dispatch
|
||||||
|
# could be overridden in this class, instead of in
|
||||||
|
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
|
||||||
|
# check to see if a subclass implements _dispatch and dispatch
|
||||||
|
# using that method if present.
|
||||||
|
response = self.server._marshaled_dispatch(
|
||||||
|
data, getattr(self, '_dispatch', None)
|
||||||
|
)
|
||||||
|
except Exception, e: # This should only happen if the module is buggy
|
||||||
|
# internal error, report as HTTP server error
|
||||||
|
self.send_response(500)
|
||||||
|
|
||||||
|
# Send information about the exception if requested
|
||||||
|
if hasattr(self.server, '_send_traceback_header') and \
|
||||||
|
self.server._send_traceback_header:
|
||||||
|
self.send_header("X-exception", str(e))
|
||||||
|
self.send_header("X-traceback", traceback.format_exc())
|
||||||
|
|
||||||
|
self.end_headers()
|
||||||
|
else:
|
||||||
|
# got a valid XML RPC response
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/xml")
|
||||||
|
self.send_header("Content-length", str(len(response)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(response)
|
||||||
|
|
||||||
|
# shut down the connection
|
||||||
|
self.wfile.flush()
|
||||||
|
self.connection.shutdown(1)
|
||||||
|
|
||||||
|
def report_404 (self):
|
||||||
|
# Report a 404 error
|
||||||
|
self.send_response(404)
|
||||||
|
response = 'No such page'
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.send_header("Content-length", str(len(response)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(response)
|
||||||
|
# shut down the connection
|
||||||
|
self.wfile.flush()
|
||||||
|
self.connection.shutdown(1)
|
||||||
|
|
||||||
|
def log_request(self, code='-', size='-'):
|
||||||
|
"""Selectively log an accepted request."""
|
||||||
|
|
||||||
|
if self.server.logRequests:
|
||||||
|
BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
|
||||||
|
|
||||||
|
class SimpleXMLRPCServer(SocketServer.TCPServer,
|
||||||
|
SimpleXMLRPCDispatcher):
|
||||||
|
"""Simple XML-RPC server.
|
||||||
|
|
||||||
|
Simple XML-RPC server that allows functions and a single instance
|
||||||
|
to be installed to handle requests. The default implementation
|
||||||
|
attempts to dispatch XML-RPC calls to the functions or instance
|
||||||
|
installed in the server. Override the _dispatch method inhereted
|
||||||
|
from SimpleXMLRPCDispatcher to change this behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
# Warning: this is for debugging purposes only! Never set this to True in
|
||||||
|
# production code, as will be sending out sensitive information (exception
|
||||||
|
# and stack trace details) when exceptions are raised inside
|
||||||
|
# SimpleXMLRPCRequestHandler.do_POST
|
||||||
|
_send_traceback_header = False
|
||||||
|
|
||||||
|
def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
|
||||||
|
logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
|
||||||
|
self.logRequests = logRequests
|
||||||
|
|
||||||
|
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||||
|
SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
|
||||||
|
|
||||||
|
# [Bug #1222790] If possible, set close-on-exec flag; if a
|
||||||
|
# method spawns a subprocess, the subprocess shouldn't have
|
||||||
|
# the listening socket open.
|
||||||
|
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
|
||||||
|
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
|
||||||
|
flags |= fcntl.FD_CLOEXEC
|
||||||
|
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
|
||||||
|
|
||||||
|
class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
|
||||||
|
"""Simple handler for XML-RPC data passed through CGI."""
|
||||||
|
|
||||||
|
def __init__(self, allow_none=False, encoding=None):
|
||||||
|
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||||
|
|
||||||
|
def handle_xmlrpc(self, request_text):
|
||||||
|
"""Handle a single XML-RPC request"""
|
||||||
|
|
||||||
|
response = self._marshaled_dispatch(request_text)
|
||||||
|
|
||||||
|
print 'Content-Type: text/xml'
|
||||||
|
print 'Content-Length: %d' % len(response)
|
||||||
|
print
|
||||||
|
sys.stdout.write(response)
|
||||||
|
|
||||||
|
def handle_get(self):
|
||||||
|
"""Handle a single HTTP GET request.
|
||||||
|
|
||||||
|
Default implementation indicates an error because
|
||||||
|
XML-RPC uses the POST method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = 400
|
||||||
|
message, explain = \
|
||||||
|
BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
|
||||||
|
|
||||||
|
response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
|
||||||
|
{
|
||||||
|
'code' : code,
|
||||||
|
'message' : message,
|
||||||
|
'explain' : explain
|
||||||
|
}
|
||||||
|
print 'Status: %d %s' % (code, message)
|
||||||
|
print 'Content-Type: text/html'
|
||||||
|
print 'Content-Length: %d' % len(response)
|
||||||
|
print
|
||||||
|
sys.stdout.write(response)
|
||||||
|
|
||||||
|
def handle_request(self, request_text = None):
|
||||||
|
"""Handle a single XML-RPC request passed through a CGI post method.
|
||||||
|
|
||||||
|
If no XML data is given then it is read from stdin. The resulting
|
||||||
|
XML-RPC response is printed to stdout along with the correct HTTP
|
||||||
|
headers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request_text is None and \
|
||||||
|
os.environ.get('REQUEST_METHOD', None) == 'GET':
|
||||||
|
self.handle_get()
|
||||||
|
else:
|
||||||
|
# POST data is normally available through stdin
|
||||||
|
if request_text is None:
|
||||||
|
request_text = sys.stdin.read()
|
||||||
|
|
||||||
|
self.handle_xmlrpc(request_text)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print 'Running XML-RPC server on port 8000'
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_function(pow)
|
||||||
|
server.register_function(lambda x,y: x+y, 'add')
|
||||||
|
server.serve_forever()
|
|
@ -0,0 +1,681 @@
|
||||||
|
"""Generic socket server classes.
|
||||||
|
|
||||||
|
This module tries to capture the various aspects of defining a server:
|
||||||
|
|
||||||
|
For socket-based servers:
|
||||||
|
|
||||||
|
- address family:
|
||||||
|
- AF_INET{,6}: IP (Internet Protocol) sockets (default)
|
||||||
|
- AF_UNIX: Unix domain sockets
|
||||||
|
- others, e.g. AF_DECNET are conceivable (see <socket.h>
|
||||||
|
- socket type:
|
||||||
|
- SOCK_STREAM (reliable stream, e.g. TCP)
|
||||||
|
- SOCK_DGRAM (datagrams, e.g. UDP)
|
||||||
|
|
||||||
|
For request-based servers (including socket-based):
|
||||||
|
|
||||||
|
- client address verification before further looking at the request
|
||||||
|
(This is actually a hook for any processing that needs to look
|
||||||
|
at the request before anything else, e.g. logging)
|
||||||
|
- how to handle multiple requests:
|
||||||
|
- synchronous (one request is handled at a time)
|
||||||
|
- forking (each request is handled by a new process)
|
||||||
|
- threading (each request is handled by a new thread)
|
||||||
|
|
||||||
|
The classes in this module favor the server type that is simplest to
|
||||||
|
write: a synchronous TCP/IP server. This is bad class design, but
|
||||||
|
save some typing. (There's also the issue that a deep class hierarchy
|
||||||
|
slows down method lookups.)
|
||||||
|
|
||||||
|
There are five classes in an inheritance diagram, four of which represent
|
||||||
|
synchronous servers of four types:
|
||||||
|
|
||||||
|
+------------+
|
||||||
|
| BaseServer |
|
||||||
|
+------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-----------+ +------------------+
|
||||||
|
| TCPServer |------->| UnixStreamServer |
|
||||||
|
+-----------+ +------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-----------+ +--------------------+
|
||||||
|
| UDPServer |------->| UnixDatagramServer |
|
||||||
|
+-----------+ +--------------------+
|
||||||
|
|
||||||
|
Note that UnixDatagramServer derives from UDPServer, not from
|
||||||
|
UnixStreamServer -- the only difference between an IP and a Unix
|
||||||
|
stream server is the address family, which is simply repeated in both
|
||||||
|
unix server classes.
|
||||||
|
|
||||||
|
Forking and threading versions of each type of server can be created
|
||||||
|
using the ForkingMixIn and ThreadingMixIn mix-in classes. For
|
||||||
|
instance, a threading UDP server class is created as follows:
|
||||||
|
|
||||||
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
||||||
|
|
||||||
|
The Mix-in class must come first, since it overrides a method defined
|
||||||
|
in UDPServer! Setting the various member variables also changes
|
||||||
|
the behavior of the underlying server mechanism.
|
||||||
|
|
||||||
|
To implement a service, you must derive a class from
|
||||||
|
BaseRequestHandler and redefine its handle() method. You can then run
|
||||||
|
various versions of the service by combining one of the server classes
|
||||||
|
with your request handler class.
|
||||||
|
|
||||||
|
The request handler class must be different for datagram or stream
|
||||||
|
services. This can be hidden by using the request handler
|
||||||
|
subclasses StreamRequestHandler or DatagramRequestHandler.
|
||||||
|
|
||||||
|
Of course, you still have to use your head!
|
||||||
|
|
||||||
|
For instance, it makes no sense to use a forking server if the service
|
||||||
|
contains state in memory that can be modified by requests (since the
|
||||||
|
modifications in the child process would never reach the initial state
|
||||||
|
kept in the parent process and passed to each child). In this case,
|
||||||
|
you can use a threading server, but you will probably have to use
|
||||||
|
locks to avoid two requests that come in nearly simultaneous to apply
|
||||||
|
conflicting changes to the server state.
|
||||||
|
|
||||||
|
On the other hand, if you are building e.g. an HTTP server, where all
|
||||||
|
data is stored externally (e.g. in the file system), a synchronous
|
||||||
|
class will essentially render the service "deaf" while one request is
|
||||||
|
being handled -- which may be for a very long time if a client is slow
|
||||||
|
to reqd all the data it has requested. Here a threading or forking
|
||||||
|
server is appropriate.
|
||||||
|
|
||||||
|
In some cases, it may be appropriate to process part of a request
|
||||||
|
synchronously, but to finish processing in a forked child depending on
|
||||||
|
the request data. This can be implemented by using a synchronous
|
||||||
|
server and doing an explicit fork in the request handler class
|
||||||
|
handle() method.
|
||||||
|
|
||||||
|
Another approach to handling multiple simultaneous requests in an
|
||||||
|
environment that supports neither threads nor fork (or where these are
|
||||||
|
too expensive or inappropriate for the service) is to maintain an
|
||||||
|
explicit table of partially finished requests and to use select() to
|
||||||
|
decide which request to work on next (or whether to handle a new
|
||||||
|
incoming request). This is particularly important for stream services
|
||||||
|
where each client can potentially be connected for a long time (if
|
||||||
|
threads or subprocesses cannot be used).
|
||||||
|
|
||||||
|
Future work:
|
||||||
|
- Standard classes for Sun RPC (which uses either UDP or TCP)
|
||||||
|
- Standard mix-in classes to implement various authentication
|
||||||
|
and encryption schemes
|
||||||
|
- Standard framework for select-based multiplexing
|
||||||
|
|
||||||
|
XXX Open problems:
|
||||||
|
- What to do with out-of-band data?
|
||||||
|
|
||||||
|
BaseServer:
|
||||||
|
- split generic "request" functionality out into BaseServer class.
|
||||||
|
Copyright (C) 2000 Luke Kenneth Casson Leighton <lkcl@samba.org>
|
||||||
|
|
||||||
|
example: read entries from a SQL database (requires overriding
|
||||||
|
get_request() to return a table entry from the database).
|
||||||
|
entry is processed by a RequestHandlerClass.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Author of the BaseServer patch: Luke Kenneth Casson Leighton
|
||||||
|
|
||||||
|
# XXX Warning!
|
||||||
|
# There is a test suite for this module, but it cannot be run by the
|
||||||
|
# standard regression test.
|
||||||
|
# To run it manually, run Lib/test/test_socketserver.py.
|
||||||
|
|
||||||
|
__version__ = "0.4"
|
||||||
|
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
except ImportError:
|
||||||
|
import dummy_threading as threading
|
||||||
|
|
||||||
|
__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
|
||||||
|
"ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
|
||||||
|
"StreamRequestHandler","DatagramRequestHandler",
|
||||||
|
"ThreadingMixIn", "ForkingMixIn"]
|
||||||
|
if hasattr(socket, "AF_UNIX"):
|
||||||
|
__all__.extend(["UnixStreamServer","UnixDatagramServer",
|
||||||
|
"ThreadingUnixStreamServer",
|
||||||
|
"ThreadingUnixDatagramServer"])
|
||||||
|
|
||||||
|
class BaseServer:
|
||||||
|
|
||||||
|
"""Base class for server classes.
|
||||||
|
|
||||||
|
Methods for the caller:
|
||||||
|
|
||||||
|
- __init__(server_address, RequestHandlerClass)
|
||||||
|
- serve_forever(poll_interval=0.5)
|
||||||
|
- shutdown()
|
||||||
|
- handle_request() # if you do not use serve_forever()
|
||||||
|
- fileno() -> int # for select()
|
||||||
|
|
||||||
|
Methods that may be overridden:
|
||||||
|
|
||||||
|
- server_bind()
|
||||||
|
- server_activate()
|
||||||
|
- get_request() -> request, client_address
|
||||||
|
- handle_timeout()
|
||||||
|
- verify_request(request, client_address)
|
||||||
|
- server_close()
|
||||||
|
- process_request(request, client_address)
|
||||||
|
- close_request(request)
|
||||||
|
- handle_error()
|
||||||
|
|
||||||
|
Methods for derived classes:
|
||||||
|
|
||||||
|
- finish_request(request, client_address)
|
||||||
|
|
||||||
|
Class variables that may be overridden by derived classes or
|
||||||
|
instances:
|
||||||
|
|
||||||
|
- timeout
|
||||||
|
- address_family
|
||||||
|
- socket_type
|
||||||
|
- allow_reuse_address
|
||||||
|
|
||||||
|
Instance variables:
|
||||||
|
|
||||||
|
- RequestHandlerClass
|
||||||
|
- socket
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
timeout = None
|
||||||
|
|
||||||
|
def __init__(self, server_address, RequestHandlerClass):
|
||||||
|
"""Constructor. May be extended, do not override."""
|
||||||
|
self.server_address = server_address
|
||||||
|
self.RequestHandlerClass = RequestHandlerClass
|
||||||
|
self.__is_shut_down = threading.Event()
|
||||||
|
self.__serving = False
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
"""Called by constructor to activate the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serve_forever(self, poll_interval=0.5):
|
||||||
|
"""Handle one request at a time until shutdown.
|
||||||
|
|
||||||
|
Polls for shutdown every poll_interval seconds. Ignores
|
||||||
|
self.timeout. If you need to do periodic tasks, do them in
|
||||||
|
another thread.
|
||||||
|
"""
|
||||||
|
self.__serving = True
|
||||||
|
self.__is_shut_down.clear()
|
||||||
|
while self.__serving:
|
||||||
|
# XXX: Consider using another file descriptor or
|
||||||
|
# connecting to the socket to wake this up instead of
|
||||||
|
# polling. Polling reduces our responsiveness to a
|
||||||
|
# shutdown request and wastes cpu at all other times.
|
||||||
|
r, w, e = select.select([self], [], [], poll_interval)
|
||||||
|
if r:
|
||||||
|
self._handle_request_noblock()
|
||||||
|
self.__is_shut_down.set()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Stops the serve_forever loop.
|
||||||
|
|
||||||
|
Blocks until the loop has finished. This must be called while
|
||||||
|
serve_forever() is running in another thread, or it will
|
||||||
|
deadlock.
|
||||||
|
"""
|
||||||
|
self.__serving = False
|
||||||
|
self.__is_shut_down.wait()
|
||||||
|
|
||||||
|
# The distinction between handling, getting, processing and
|
||||||
|
# finishing a request is fairly arbitrary. Remember:
|
||||||
|
#
|
||||||
|
# - handle_request() is the top-level call. It calls
|
||||||
|
# select, get_request(), verify_request() and process_request()
|
||||||
|
# - get_request() is different for stream or datagram sockets
|
||||||
|
# - process_request() is the place that may fork a new process
|
||||||
|
# or create a new thread to finish the request
|
||||||
|
# - finish_request() instantiates the request handler class;
|
||||||
|
# this constructor will handle the request all by itself
|
||||||
|
|
||||||
|
def handle_request(self):
|
||||||
|
"""Handle one request, possibly blocking.
|
||||||
|
|
||||||
|
Respects self.timeout.
|
||||||
|
"""
|
||||||
|
# Support people who used socket.settimeout() to escape
|
||||||
|
# handle_request before self.timeout was available.
|
||||||
|
timeout = self.socket.gettimeout()
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
elif self.timeout is not None:
|
||||||
|
timeout = min(timeout, self.timeout)
|
||||||
|
fd_sets = select.select([self], [], [], timeout)
|
||||||
|
if not fd_sets[0]:
|
||||||
|
self.handle_timeout()
|
||||||
|
return
|
||||||
|
self._handle_request_noblock()
|
||||||
|
|
||||||
|
def _handle_request_noblock(self):
|
||||||
|
"""Handle one request, without blocking.
|
||||||
|
|
||||||
|
I assume that select.select has returned that the socket is
|
||||||
|
readable before this function was called, so there should be
|
||||||
|
no risk of blocking in get_request().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
request, client_address = self.get_request()
|
||||||
|
except socket.error:
|
||||||
|
return
|
||||||
|
if self.verify_request(request, client_address):
|
||||||
|
try:
|
||||||
|
self.process_request(request, client_address)
|
||||||
|
except:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def handle_timeout(self):
|
||||||
|
"""Called if no new request arrives within self.timeout.
|
||||||
|
|
||||||
|
Overridden by ForkingMixIn.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def verify_request(self, request, client_address):
|
||||||
|
"""Verify the request. May be overridden.
|
||||||
|
|
||||||
|
Return True if we should proceed with this request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Call finish_request.
|
||||||
|
|
||||||
|
Overridden by ForkingMixIn and ThreadingMixIn.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def server_close(self):
|
||||||
|
"""Called to clean-up the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish_request(self, request, client_address):
|
||||||
|
"""Finish one request by instantiating RequestHandlerClass."""
|
||||||
|
self.RequestHandlerClass(request, client_address, self)
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
"""Called to clean up an individual request."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_error(self, request, client_address):
|
||||||
|
"""Handle an error gracefully. May be overridden.
|
||||||
|
|
||||||
|
The default is to print a traceback and continue.
|
||||||
|
|
||||||
|
"""
|
||||||
|
print '-'*40
|
||||||
|
print 'Exception happened during processing of request from',
|
||||||
|
print client_address
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc() # XXX But this goes to stderr!
|
||||||
|
print '-'*40
|
||||||
|
|
||||||
|
|
||||||
|
class TCPServer(BaseServer):
|
||||||
|
|
||||||
|
"""Base class for various socket-based server classes.
|
||||||
|
|
||||||
|
Defaults to synchronous IP stream (i.e., TCP).
|
||||||
|
|
||||||
|
Methods for the caller:
|
||||||
|
|
||||||
|
- __init__(server_address, RequestHandlerClass, bind_and_activate=True)
|
||||||
|
- serve_forever(poll_interval=0.5)
|
||||||
|
- shutdown()
|
||||||
|
- handle_request() # if you don't use serve_forever()
|
||||||
|
- fileno() -> int # for select()
|
||||||
|
|
||||||
|
Methods that may be overridden:
|
||||||
|
|
||||||
|
- server_bind()
|
||||||
|
- server_activate()
|
||||||
|
- get_request() -> request, client_address
|
||||||
|
- handle_timeout()
|
||||||
|
- verify_request(request, client_address)
|
||||||
|
- process_request(request, client_address)
|
||||||
|
- close_request(request)
|
||||||
|
- handle_error()
|
||||||
|
|
||||||
|
Methods for derived classes:
|
||||||
|
|
||||||
|
- finish_request(request, client_address)
|
||||||
|
|
||||||
|
Class variables that may be overridden by derived classes or
|
||||||
|
instances:
|
||||||
|
|
||||||
|
- timeout
|
||||||
|
- address_family
|
||||||
|
- socket_type
|
||||||
|
- request_queue_size (only for stream sockets)
|
||||||
|
- allow_reuse_address
|
||||||
|
|
||||||
|
Instance variables:
|
||||||
|
|
||||||
|
- server_address
|
||||||
|
- RequestHandlerClass
|
||||||
|
- socket
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
address_family = socket.AF_INET
|
||||||
|
|
||||||
|
socket_type = socket.SOCK_STREAM
|
||||||
|
|
||||||
|
request_queue_size = 5
|
||||||
|
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
|
||||||
|
"""Constructor. May be extended, do not override."""
|
||||||
|
BaseServer.__init__(self, server_address, RequestHandlerClass)
|
||||||
|
self.socket = socket.socket(self.address_family,
|
||||||
|
self.socket_type)
|
||||||
|
if bind_and_activate:
|
||||||
|
self.server_bind()
|
||||||
|
self.server_activate()
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
"""Called by constructor to bind the socket.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.allow_reuse_address:
|
||||||
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.socket.bind(self.server_address)
|
||||||
|
self.server_address = self.socket.getsockname()
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
"""Called by constructor to activate the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.socket.listen(self.request_queue_size)
|
||||||
|
|
||||||
|
def server_close(self):
|
||||||
|
"""Called to clean-up the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
"""Return socket file number.
|
||||||
|
|
||||||
|
Interface required by select().
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.socket.fileno()
|
||||||
|
|
||||||
|
def get_request(self):
|
||||||
|
"""Get the request and client address from the socket.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.socket.accept()
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
"""Called to clean up an individual request."""
|
||||||
|
request.close()
|
||||||
|
|
||||||
|
|
||||||
|
class UDPServer(TCPServer):
|
||||||
|
|
||||||
|
"""UDP server class."""
|
||||||
|
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
socket_type = socket.SOCK_DGRAM
|
||||||
|
|
||||||
|
max_packet_size = 8192
|
||||||
|
|
||||||
|
def get_request(self):
|
||||||
|
data, client_addr = self.socket.recvfrom(self.max_packet_size)
|
||||||
|
return (data, self.socket), client_addr
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
# No need to call listen() for UDP.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
# No need to close anything.
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ForkingMixIn:
|
||||||
|
|
||||||
|
"""Mix-in class to handle each request in a new process."""
|
||||||
|
|
||||||
|
timeout = 300
|
||||||
|
active_children = None
|
||||||
|
max_children = 40
|
||||||
|
|
||||||
|
def collect_children(self):
|
||||||
|
"""Internal routine to wait for children that have exited."""
|
||||||
|
if self.active_children is None: return
|
||||||
|
while len(self.active_children) >= self.max_children:
|
||||||
|
# XXX: This will wait for any child process, not just ones
|
||||||
|
# spawned by this library. This could confuse other
|
||||||
|
# libraries that expect to be able to wait for their own
|
||||||
|
# children.
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(0, options=0)
|
||||||
|
except os.error:
|
||||||
|
pid = None
|
||||||
|
if pid not in self.active_children: continue
|
||||||
|
self.active_children.remove(pid)
|
||||||
|
|
||||||
|
# XXX: This loop runs more system calls than it ought
|
||||||
|
# to. There should be a way to put the active_children into a
|
||||||
|
# process group and then use os.waitpid(-pgid) to wait for any
|
||||||
|
# of that set, but I couldn't find a way to allocate pgids
|
||||||
|
# that couldn't collide.
|
||||||
|
for child in self.active_children:
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(child, os.WNOHANG)
|
||||||
|
except os.error:
|
||||||
|
pid = None
|
||||||
|
if not pid: continue
|
||||||
|
try:
|
||||||
|
self.active_children.remove(pid)
|
||||||
|
except ValueError, e:
|
||||||
|
raise ValueError('%s. x=%d and list=%r' % (e.message, pid,
|
||||||
|
self.active_children))
|
||||||
|
|
||||||
|
def handle_timeout(self):
|
||||||
|
"""Wait for zombies after self.timeout seconds of inactivity.
|
||||||
|
|
||||||
|
May be extended, do not override.
|
||||||
|
"""
|
||||||
|
self.collect_children()
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Fork a new subprocess to process the request."""
|
||||||
|
self.collect_children()
|
||||||
|
pid = os.fork()
|
||||||
|
if pid:
|
||||||
|
# Parent process
|
||||||
|
if self.active_children is None:
|
||||||
|
self.active_children = []
|
||||||
|
self.active_children.append(pid)
|
||||||
|
self.close_request(request)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Child process.
|
||||||
|
# This must never return, hence os._exit()!
|
||||||
|
try:
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
os._exit(0)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
finally:
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadingMixIn:
|
||||||
|
"""Mix-in class to handle each request in a new thread."""
|
||||||
|
|
||||||
|
# Decides how threads will act upon termination of the
|
||||||
|
# main process
|
||||||
|
daemon_threads = False
|
||||||
|
|
||||||
|
def process_request_thread(self, request, client_address):
|
||||||
|
"""Same as in BaseServer but as a thread.
|
||||||
|
|
||||||
|
In addition, exception handling is done here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
except:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Start a new thread to process the request."""
|
||||||
|
t = threading.Thread(target = self.process_request_thread,
|
||||||
|
args = (request, client_address))
|
||||||
|
if self.daemon_threads:
|
||||||
|
t.setDaemon (1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
|
||||||
|
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
|
||||||
|
|
||||||
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
||||||
|
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
|
||||||
|
|
||||||
|
if hasattr(socket, 'AF_UNIX'):
|
||||||
|
|
||||||
|
class UnixStreamServer(TCPServer):
|
||||||
|
address_family = socket.AF_UNIX
|
||||||
|
|
||||||
|
class UnixDatagramServer(UDPServer):
|
||||||
|
address_family = socket.AF_UNIX
|
||||||
|
|
||||||
|
class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
|
||||||
|
|
||||||
|
class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
|
||||||
|
|
||||||
|
class BaseRequestHandler:
|
||||||
|
|
||||||
|
"""Base class for request handler classes.
|
||||||
|
|
||||||
|
This class is instantiated for each request to be handled. The
|
||||||
|
constructor sets the instance variables request, client_address
|
||||||
|
and server, and then calls the handle() method. To implement a
|
||||||
|
specific service, all you need to do is to derive a class which
|
||||||
|
defines a handle() method.
|
||||||
|
|
||||||
|
The handle() method can find the request as self.request, the
|
||||||
|
client address as self.client_address, and the server (in case it
|
||||||
|
needs access to per-server information) as self.server. Since a
|
||||||
|
separate instance is created for each request, the handle() method
|
||||||
|
can define arbitrary other instance variariables.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, client_address, server):
|
||||||
|
self.request = request
|
||||||
|
self.client_address = client_address
|
||||||
|
self.server = server
|
||||||
|
try:
|
||||||
|
self.setup()
|
||||||
|
self.handle()
|
||||||
|
self.finish()
|
||||||
|
finally:
|
||||||
|
sys.exc_traceback = None # Help garbage collection
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# The following two classes make it possible to use the same service
|
||||||
|
# class for stream or datagram servers.
|
||||||
|
# Each class sets up these instance variables:
|
||||||
|
# - rfile: a file object from which receives the request is read
|
||||||
|
# - wfile: a file object to which the reply is written
|
||||||
|
# When the handle() method returns, wfile is flushed properly
|
||||||
|
|
||||||
|
|
||||||
|
class StreamRequestHandler(BaseRequestHandler):
|
||||||
|
|
||||||
|
"""Define self.rfile and self.wfile for stream sockets."""
|
||||||
|
|
||||||
|
# Default buffer sizes for rfile, wfile.
|
||||||
|
# We default rfile to buffered because otherwise it could be
|
||||||
|
# really slow for large data (a getc() call per byte); we make
|
||||||
|
# wfile unbuffered because (a) often after a write() we want to
|
||||||
|
# read and we need to flush the line; (b) big writes to unbuffered
|
||||||
|
# files are typically optimized by stdio even when big reads
|
||||||
|
# aren't.
|
||||||
|
rbufsize = -1
|
||||||
|
wbufsize = 0
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.connection = self.request
|
||||||
|
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||||
|
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
if not self.wfile.closed:
|
||||||
|
self.wfile.flush()
|
||||||
|
self.wfile.close()
|
||||||
|
self.rfile.close()
|
||||||
|
|
||||||
|
|
||||||
|
class DatagramRequestHandler(BaseRequestHandler):
|
||||||
|
|
||||||
|
# XXX Regrettably, I cannot get this working on Linux;
|
||||||
|
# s.recvfrom() doesn't return a meaningful client address.
|
||||||
|
|
||||||
|
"""Define self.rfile and self.wfile for datagram sockets."""
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
try:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from StringIO import StringIO
|
||||||
|
self.packet, self.socket = self.request
|
||||||
|
self.rfile = StringIO(self.packet)
|
||||||
|
self.wfile = StringIO()
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
self.socket.sendto(self.wfile.getvalue(), self.client_address)
|
5
setup.py
5
setup.py
|
@ -133,6 +133,11 @@ def data_files():
|
||||||
opj('bin', 'server.pkey'),
|
opj('bin', 'server.pkey'),
|
||||||
opj('bin', 'server.cert')]))
|
opj('bin', 'server.cert')]))
|
||||||
|
|
||||||
|
if sys.version_info[0:2] == (2,5):
|
||||||
|
files.append((openerp_site_packages, [ opj('python25-compat','BaseHTTPServer.py'),
|
||||||
|
opj('python25-compat','SimpleXMLRPCServer.py'),
|
||||||
|
opj('python25-compat','SocketServer.py')]))
|
||||||
|
|
||||||
for (addonname, add_path) in find_addons():
|
for (addonname, add_path) in find_addons():
|
||||||
addon_path = opj('lib', 'python%s' % py_short_version, 'site-packages', 'openerp-server','addons', addonname)
|
addon_path = opj('lib', 'python%s' % py_short_version, 'site-packages', 'openerp-server','addons', addonname)
|
||||||
pathfiles = []
|
pathfiles = []
|
||||||
|
|
Loading…
Reference in New Issue