jpayne@68: """HTTP server classes. jpayne@68: jpayne@68: Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see jpayne@68: SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, jpayne@68: and CGIHTTPRequestHandler for CGI scripts. jpayne@68: jpayne@68: It does, however, optionally implement HTTP/1.1 persistent connections, jpayne@68: as of version 0.3. jpayne@68: jpayne@68: Notes on CGIHTTPRequestHandler jpayne@68: ------------------------------ jpayne@68: jpayne@68: This class implements GET and POST requests to cgi-bin scripts. jpayne@68: jpayne@68: If the os.fork() function is not present (e.g. on Windows), jpayne@68: subprocess.Popen() is used as a fallback, with slightly altered semantics. jpayne@68: jpayne@68: In all cases, the implementation is intentionally naive -- all jpayne@68: requests are executed synchronously. jpayne@68: jpayne@68: SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL jpayne@68: -- it may execute arbitrary Python code or external programs. jpayne@68: jpayne@68: Note that status code 200 is sent prior to execution of a CGI script, so jpayne@68: scripts cannot send other status codes such as 302 (redirect). jpayne@68: jpayne@68: XXX To do: jpayne@68: jpayne@68: - log requests even later (to capture byte count) jpayne@68: - log user-agent header and other interesting goodies jpayne@68: - send error log to separate file jpayne@68: """ jpayne@68: jpayne@68: jpayne@68: # See also: jpayne@68: # jpayne@68: # HTTP Working Group T. Berners-Lee jpayne@68: # INTERNET-DRAFT R. T. Fielding jpayne@68: # H. Frystyk Nielsen jpayne@68: # Expires September 8, 1995 March 8, 1995 jpayne@68: # jpayne@68: # URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt jpayne@68: # jpayne@68: # and jpayne@68: # jpayne@68: # Network Working Group R. Fielding jpayne@68: # Request for Comments: 2616 et al jpayne@68: # Obsoletes: 2068 June 1999 jpayne@68: # Category: Standards Track jpayne@68: # jpayne@68: # URL: http://www.faqs.org/rfcs/rfc2616.html jpayne@68: jpayne@68: # Log files jpayne@68: # --------- jpayne@68: # jpayne@68: # Here's a quote from the NCSA httpd docs about log file format. jpayne@68: # jpayne@68: # | The logfile format is as follows. Each line consists of: jpayne@68: # | jpayne@68: # | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb jpayne@68: # | jpayne@68: # | host: Either the DNS name or the IP number of the remote client jpayne@68: # | rfc931: Any information returned by identd for this person, jpayne@68: # | - otherwise. jpayne@68: # | authuser: If user sent a userid for authentication, the user name, jpayne@68: # | - otherwise. jpayne@68: # | DD: Day jpayne@68: # | Mon: Month (calendar name) jpayne@68: # | YYYY: Year jpayne@68: # | hh: hour (24-hour format, the machine's timezone) jpayne@68: # | mm: minutes jpayne@68: # | ss: seconds jpayne@68: # | request: The first line of the HTTP request as sent by the client. jpayne@68: # | ddd: the status code returned by the server, - if not available. jpayne@68: # | bbbb: the total number of bytes sent, jpayne@68: # | *not including the HTTP/1.0 header*, - if not available jpayne@68: # | jpayne@68: # | You can determine the name of the file accessed through request. jpayne@68: # jpayne@68: # (Actually, the latter is only true if you know the server configuration jpayne@68: # at the time the request was made!) jpayne@68: jpayne@68: __version__ = "0.6" jpayne@68: jpayne@68: __all__ = [ jpayne@68: "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", jpayne@68: "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler", jpayne@68: ] jpayne@68: jpayne@68: import copy jpayne@68: import datetime jpayne@68: import email.utils jpayne@68: import html jpayne@68: import http.client jpayne@68: import io jpayne@68: import mimetypes jpayne@68: import os jpayne@68: import posixpath jpayne@68: import select jpayne@68: import shutil jpayne@68: import socket # For gethostbyaddr() jpayne@68: import socketserver jpayne@68: import sys jpayne@68: import time jpayne@68: import urllib.parse jpayne@68: from functools import partial jpayne@68: jpayne@68: from http import HTTPStatus jpayne@68: jpayne@68: jpayne@68: # Default error message template jpayne@68: DEFAULT_ERROR_MESSAGE = """\ jpayne@68: jpayne@68: jpayne@68: jpayne@68: jpayne@68: Error response jpayne@68: jpayne@68: jpayne@68:

Error response

jpayne@68:

Error code: %(code)d

jpayne@68:

Message: %(message)s.

jpayne@68:

Error code explanation: %(code)s - %(explain)s.

jpayne@68: jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" jpayne@68: jpayne@68: class HTTPServer(socketserver.TCPServer): jpayne@68: jpayne@68: allow_reuse_address = 1 # Seems to make sense in testing environment jpayne@68: jpayne@68: def server_bind(self): jpayne@68: """Override server_bind to store the server name.""" jpayne@68: socketserver.TCPServer.server_bind(self) jpayne@68: host, port = self.server_address[:2] jpayne@68: self.server_name = socket.getfqdn(host) jpayne@68: self.server_port = port jpayne@68: jpayne@68: jpayne@68: class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): jpayne@68: daemon_threads = True jpayne@68: jpayne@68: jpayne@68: class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): jpayne@68: jpayne@68: """HTTP request handler base class. jpayne@68: jpayne@68: The following explanation of HTTP serves to guide you through the jpayne@68: code as well as to expose any misunderstandings I may have about jpayne@68: HTTP (so you don't need to read the code to figure out I'm wrong jpayne@68: :-). jpayne@68: jpayne@68: HTTP (HyperText Transfer Protocol) is an extensible protocol on jpayne@68: top of a reliable stream transport (e.g. TCP/IP). The protocol jpayne@68: recognizes three parts to a request: jpayne@68: jpayne@68: 1. One line identifying the request type and path jpayne@68: 2. An optional set of RFC-822-style headers jpayne@68: 3. An optional data part jpayne@68: jpayne@68: The headers and data are separated by a blank line. jpayne@68: jpayne@68: The first line of the request has the form jpayne@68: jpayne@68: jpayne@68: jpayne@68: where is a (case-sensitive) keyword such as GET or POST, jpayne@68: is a string containing path information for the request, jpayne@68: and should be the string "HTTP/1.0" or "HTTP/1.1". jpayne@68: is encoded using the URL encoding scheme (using %xx to signify jpayne@68: the ASCII character with hex code xx). jpayne@68: jpayne@68: The specification specifies that lines are separated by CRLF but jpayne@68: for compatibility with the widest range of clients recommends jpayne@68: servers also handle LF. Similarly, whitespace in the request line jpayne@68: is treated sensibly (allowing multiple spaces between components jpayne@68: and allowing trailing whitespace). jpayne@68: jpayne@68: Similarly, for output, lines ought to be separated by CRLF pairs jpayne@68: but most clients grok LF characters just fine. jpayne@68: jpayne@68: If the first line of the request has the form jpayne@68: jpayne@68: jpayne@68: jpayne@68: (i.e. is left out) then this is assumed to be an HTTP jpayne@68: 0.9 request; this form has no optional headers and data part and jpayne@68: the reply consists of just the data. jpayne@68: jpayne@68: The reply form of the HTTP 1.x protocol again has three parts: jpayne@68: jpayne@68: 1. One line giving the response code jpayne@68: 2. An optional set of RFC-822-style headers jpayne@68: 3. The data jpayne@68: jpayne@68: Again, the headers and data are separated by a blank line. jpayne@68: jpayne@68: The response code line has the form jpayne@68: jpayne@68: jpayne@68: jpayne@68: where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), jpayne@68: is a 3-digit response code indicating success or jpayne@68: failure of the request, and is an optional jpayne@68: human-readable string explaining what the response code means. jpayne@68: jpayne@68: This server parses the request and the headers, and then calls a jpayne@68: function specific to the request type (). Specifically, jpayne@68: a request SPAM will be handled by a method do_SPAM(). If no jpayne@68: such method exists the server sends an error response to the jpayne@68: client. If it exists, it is called with no arguments: jpayne@68: jpayne@68: do_SPAM() jpayne@68: jpayne@68: Note that the request name is case sensitive (i.e. SPAM and spam jpayne@68: are different requests). jpayne@68: jpayne@68: The various request details are stored in instance variables: jpayne@68: jpayne@68: - client_address is the client IP address in the form (host, jpayne@68: port); jpayne@68: jpayne@68: - command, path and version are the broken-down request line; jpayne@68: jpayne@68: - headers is an instance of email.message.Message (or a derived jpayne@68: class) containing the header information; jpayne@68: jpayne@68: - rfile is a file object open for reading positioned at the jpayne@68: start of the optional input data part; jpayne@68: jpayne@68: - wfile is a file object open for writing. jpayne@68: jpayne@68: IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! jpayne@68: jpayne@68: The first thing to be written must be the response line. Then jpayne@68: follow 0 or more header lines, then a blank line, and then the jpayne@68: actual data (if any). The meaning of the header lines depends on jpayne@68: the command executed by the server; in most cases, when data is jpayne@68: returned, there should be at least one header line of the form jpayne@68: jpayne@68: Content-type: / jpayne@68: jpayne@68: where and should be registered MIME types, jpayne@68: e.g. "text/html" or "text/plain". jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: # The Python system version, truncated to its first component. jpayne@68: sys_version = "Python/" + sys.version.split()[0] jpayne@68: jpayne@68: # The server software version. You may want to override this. jpayne@68: # The format is multiple whitespace-separated strings, jpayne@68: # where each string is of the form name[/version]. jpayne@68: server_version = "BaseHTTP/" + __version__ jpayne@68: jpayne@68: error_message_format = DEFAULT_ERROR_MESSAGE jpayne@68: error_content_type = DEFAULT_ERROR_CONTENT_TYPE jpayne@68: jpayne@68: # The default request version. This only affects responses up until jpayne@68: # the point where the request line is parsed, so it mainly decides what jpayne@68: # the client gets back when sending a malformed request line. jpayne@68: # Most web servers default to HTTP 0.9, i.e. don't send a status line. jpayne@68: default_request_version = "HTTP/0.9" jpayne@68: jpayne@68: def parse_request(self): jpayne@68: """Parse a request (internal). jpayne@68: jpayne@68: The request should be stored in self.raw_requestline; the results jpayne@68: are in self.command, self.path, self.request_version and jpayne@68: self.headers. jpayne@68: jpayne@68: Return True for success, False for failure; on failure, any relevant jpayne@68: error response has already been sent back. jpayne@68: jpayne@68: """ jpayne@68: self.command = None # set in case of error on the first line jpayne@68: self.request_version = version = self.default_request_version jpayne@68: self.close_connection = True jpayne@68: requestline = str(self.raw_requestline, 'iso-8859-1') jpayne@68: requestline = requestline.rstrip('\r\n') jpayne@68: self.requestline = requestline jpayne@68: words = requestline.split() jpayne@68: if len(words) == 0: jpayne@68: return False jpayne@68: jpayne@68: if len(words) >= 3: # Enough to determine protocol version jpayne@68: version = words[-1] jpayne@68: try: jpayne@68: if not version.startswith('HTTP/'): jpayne@68: raise ValueError jpayne@68: base_version_number = version.split('/', 1)[1] jpayne@68: version_number = base_version_number.split(".") jpayne@68: # RFC 2145 section 3.1 says there can be only one "." and jpayne@68: # - major and minor numbers MUST be treated as jpayne@68: # separate integers; jpayne@68: # - HTTP/2.4 is a lower version than HTTP/2.13, which in jpayne@68: # turn is lower than HTTP/12.3; jpayne@68: # - Leading zeros MUST be ignored by recipients. jpayne@68: if len(version_number) != 2: jpayne@68: raise ValueError jpayne@68: version_number = int(version_number[0]), int(version_number[1]) jpayne@68: except (ValueError, IndexError): jpayne@68: self.send_error( jpayne@68: HTTPStatus.BAD_REQUEST, jpayne@68: "Bad request version (%r)" % version) jpayne@68: return False jpayne@68: if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": jpayne@68: self.close_connection = False jpayne@68: if version_number >= (2, 0): jpayne@68: self.send_error( jpayne@68: HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, jpayne@68: "Invalid HTTP version (%s)" % base_version_number) jpayne@68: return False jpayne@68: self.request_version = version jpayne@68: jpayne@68: if not 2 <= len(words) <= 3: jpayne@68: self.send_error( jpayne@68: HTTPStatus.BAD_REQUEST, jpayne@68: "Bad request syntax (%r)" % requestline) jpayne@68: return False jpayne@68: command, path = words[:2] jpayne@68: if len(words) == 2: jpayne@68: self.close_connection = True jpayne@68: if command != 'GET': jpayne@68: self.send_error( jpayne@68: HTTPStatus.BAD_REQUEST, jpayne@68: "Bad HTTP/0.9 request type (%r)" % command) jpayne@68: return False jpayne@68: self.command, self.path = command, path jpayne@68: jpayne@68: # Examine the headers and look for a Connection directive. jpayne@68: try: jpayne@68: self.headers = http.client.parse_headers(self.rfile, jpayne@68: _class=self.MessageClass) jpayne@68: except http.client.LineTooLong as err: jpayne@68: self.send_error( jpayne@68: HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, jpayne@68: "Line too long", jpayne@68: str(err)) jpayne@68: return False jpayne@68: except http.client.HTTPException as err: jpayne@68: self.send_error( jpayne@68: HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, jpayne@68: "Too many headers", jpayne@68: str(err) jpayne@68: ) jpayne@68: return False jpayne@68: jpayne@68: conntype = self.headers.get('Connection', "") jpayne@68: if conntype.lower() == 'close': jpayne@68: self.close_connection = True jpayne@68: elif (conntype.lower() == 'keep-alive' and jpayne@68: self.protocol_version >= "HTTP/1.1"): jpayne@68: self.close_connection = False jpayne@68: # Examine the headers and look for an Expect directive jpayne@68: expect = self.headers.get('Expect', "") jpayne@68: if (expect.lower() == "100-continue" and jpayne@68: self.protocol_version >= "HTTP/1.1" and jpayne@68: self.request_version >= "HTTP/1.1"): jpayne@68: if not self.handle_expect_100(): jpayne@68: return False jpayne@68: return True jpayne@68: jpayne@68: def handle_expect_100(self): jpayne@68: """Decide what to do with an "Expect: 100-continue" header. jpayne@68: jpayne@68: If the client is expecting a 100 Continue response, we must jpayne@68: respond with either a 100 Continue or a final response before jpayne@68: waiting for the request body. The default is to always respond jpayne@68: with a 100 Continue. You can behave differently (for example, jpayne@68: reject unauthorized requests) by overriding this method. jpayne@68: jpayne@68: This method should either return True (possibly after sending jpayne@68: a 100 Continue response) or send an error response and return jpayne@68: False. jpayne@68: jpayne@68: """ jpayne@68: self.send_response_only(HTTPStatus.CONTINUE) jpayne@68: self.end_headers() jpayne@68: return True jpayne@68: jpayne@68: def handle_one_request(self): jpayne@68: """Handle a single HTTP request. jpayne@68: jpayne@68: You normally don't need to override this method; see the class jpayne@68: __doc__ string for information on how to handle specific HTTP jpayne@68: commands such as GET and POST. jpayne@68: jpayne@68: """ jpayne@68: try: jpayne@68: self.raw_requestline = self.rfile.readline(65537) jpayne@68: if len(self.raw_requestline) > 65536: jpayne@68: self.requestline = '' jpayne@68: self.request_version = '' jpayne@68: self.command = '' jpayne@68: self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) jpayne@68: return jpayne@68: if not self.raw_requestline: jpayne@68: self.close_connection = True jpayne@68: return jpayne@68: if not self.parse_request(): jpayne@68: # An error code has been sent, just exit jpayne@68: return jpayne@68: mname = 'do_' + self.command jpayne@68: if not hasattr(self, mname): jpayne@68: self.send_error( jpayne@68: HTTPStatus.NOT_IMPLEMENTED, jpayne@68: "Unsupported method (%r)" % self.command) jpayne@68: return jpayne@68: method = getattr(self, mname) jpayne@68: method() jpayne@68: self.wfile.flush() #actually send the response if not already done. jpayne@68: except socket.timeout as e: jpayne@68: #a read or a write timed out. Discard this connection jpayne@68: self.log_error("Request timed out: %r", e) jpayne@68: self.close_connection = True jpayne@68: return jpayne@68: jpayne@68: def handle(self): jpayne@68: """Handle multiple requests if necessary.""" jpayne@68: self.close_connection = True jpayne@68: jpayne@68: self.handle_one_request() jpayne@68: while not self.close_connection: jpayne@68: self.handle_one_request() jpayne@68: jpayne@68: def send_error(self, code, message=None, explain=None): jpayne@68: """Send and log an error reply. jpayne@68: jpayne@68: Arguments are jpayne@68: * code: an HTTP error code jpayne@68: 3 digits jpayne@68: * message: a simple optional 1 line reason phrase. jpayne@68: *( HTAB / SP / VCHAR / %x80-FF ) jpayne@68: defaults to short entry matching the response code jpayne@68: * explain: a detailed message defaults to the long entry jpayne@68: matching the response code. jpayne@68: jpayne@68: This sends an error response (so it must be called before any jpayne@68: output has been generated), logs the error, and finally sends jpayne@68: a piece of HTML explaining the error to the user. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: try: jpayne@68: shortmsg, longmsg = self.responses[code] jpayne@68: except KeyError: jpayne@68: shortmsg, longmsg = '???', '???' jpayne@68: if message is None: jpayne@68: message = shortmsg jpayne@68: if explain is None: jpayne@68: explain = longmsg jpayne@68: self.log_error("code %d, message %s", code, message) jpayne@68: self.send_response(code, message) jpayne@68: self.send_header('Connection', 'close') jpayne@68: jpayne@68: # Message body is omitted for cases described in: jpayne@68: # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) jpayne@68: # - RFC7231: 6.3.6. 205(Reset Content) jpayne@68: body = None jpayne@68: if (code >= 200 and jpayne@68: code not in (HTTPStatus.NO_CONTENT, jpayne@68: HTTPStatus.RESET_CONTENT, jpayne@68: HTTPStatus.NOT_MODIFIED)): jpayne@68: # HTML encode to prevent Cross Site Scripting attacks jpayne@68: # (see bug #1100201) jpayne@68: content = (self.error_message_format % { jpayne@68: 'code': code, jpayne@68: 'message': html.escape(message, quote=False), jpayne@68: 'explain': html.escape(explain, quote=False) jpayne@68: }) jpayne@68: body = content.encode('UTF-8', 'replace') jpayne@68: self.send_header("Content-Type", self.error_content_type) jpayne@68: self.send_header('Content-Length', str(len(body))) jpayne@68: self.end_headers() jpayne@68: jpayne@68: if self.command != 'HEAD' and body: jpayne@68: self.wfile.write(body) jpayne@68: jpayne@68: def send_response(self, code, message=None): jpayne@68: """Add the response header to the headers buffer and log the jpayne@68: response code. jpayne@68: jpayne@68: Also send two standard headers with the server software jpayne@68: version and the current date. jpayne@68: jpayne@68: """ jpayne@68: self.log_request(code) jpayne@68: self.send_response_only(code, message) jpayne@68: self.send_header('Server', self.version_string()) jpayne@68: self.send_header('Date', self.date_time_string()) jpayne@68: jpayne@68: def send_response_only(self, code, message=None): jpayne@68: """Send the response header only.""" jpayne@68: if self.request_version != 'HTTP/0.9': jpayne@68: if message is None: jpayne@68: if code in self.responses: jpayne@68: message = self.responses[code][0] jpayne@68: else: jpayne@68: message = '' jpayne@68: if not hasattr(self, '_headers_buffer'): jpayne@68: self._headers_buffer = [] jpayne@68: self._headers_buffer.append(("%s %d %s\r\n" % jpayne@68: (self.protocol_version, code, message)).encode( jpayne@68: 'latin-1', 'strict')) jpayne@68: jpayne@68: def send_header(self, keyword, value): jpayne@68: """Send a MIME header to the headers buffer.""" jpayne@68: if self.request_version != 'HTTP/0.9': jpayne@68: if not hasattr(self, '_headers_buffer'): jpayne@68: self._headers_buffer = [] jpayne@68: self._headers_buffer.append( jpayne@68: ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) jpayne@68: jpayne@68: if keyword.lower() == 'connection': jpayne@68: if value.lower() == 'close': jpayne@68: self.close_connection = True jpayne@68: elif value.lower() == 'keep-alive': jpayne@68: self.close_connection = False jpayne@68: jpayne@68: def end_headers(self): jpayne@68: """Send the blank line ending the MIME headers.""" jpayne@68: if self.request_version != 'HTTP/0.9': jpayne@68: self._headers_buffer.append(b"\r\n") jpayne@68: self.flush_headers() jpayne@68: jpayne@68: def flush_headers(self): jpayne@68: if hasattr(self, '_headers_buffer'): jpayne@68: self.wfile.write(b"".join(self._headers_buffer)) jpayne@68: self._headers_buffer = [] jpayne@68: jpayne@68: def log_request(self, code='-', size='-'): jpayne@68: """Log an accepted request. jpayne@68: jpayne@68: This is called by send_response(). jpayne@68: jpayne@68: """ jpayne@68: if isinstance(code, HTTPStatus): jpayne@68: code = code.value jpayne@68: self.log_message('"%s" %s %s', jpayne@68: self.requestline, str(code), str(size)) jpayne@68: jpayne@68: def log_error(self, format, *args): jpayne@68: """Log an error. jpayne@68: jpayne@68: This is called when a request cannot be fulfilled. By jpayne@68: default it passes the message on to log_message(). jpayne@68: jpayne@68: Arguments are the same as for log_message(). jpayne@68: jpayne@68: XXX This should go to the separate error log. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: self.log_message(format, *args) jpayne@68: jpayne@68: def log_message(self, format, *args): jpayne@68: """Log an arbitrary message. jpayne@68: jpayne@68: This is used by all other logging functions. Override jpayne@68: it if you have specific logging wishes. jpayne@68: jpayne@68: The first argument, FORMAT, is a format string for the jpayne@68: message to be logged. If the format string contains jpayne@68: any % escapes requiring parameters, they should be jpayne@68: specified as subsequent arguments (it's just like jpayne@68: printf!). jpayne@68: jpayne@68: The client ip and current date/time are prefixed to jpayne@68: every message. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: sys.stderr.write("%s - - [%s] %s\n" % jpayne@68: (self.address_string(), jpayne@68: self.log_date_time_string(), jpayne@68: format%args)) jpayne@68: jpayne@68: def version_string(self): jpayne@68: """Return the server software version string.""" jpayne@68: return self.server_version + ' ' + self.sys_version jpayne@68: jpayne@68: def date_time_string(self, timestamp=None): jpayne@68: """Return the current date and time formatted for a message header.""" jpayne@68: if timestamp is None: jpayne@68: timestamp = time.time() jpayne@68: return email.utils.formatdate(timestamp, usegmt=True) jpayne@68: jpayne@68: def log_date_time_string(self): jpayne@68: """Return the current time formatted for logging.""" jpayne@68: now = time.time() jpayne@68: year, month, day, hh, mm, ss, x, y, z = time.localtime(now) jpayne@68: s = "%02d/%3s/%04d %02d:%02d:%02d" % ( jpayne@68: day, self.monthname[month], year, hh, mm, ss) jpayne@68: return s jpayne@68: jpayne@68: weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] jpayne@68: jpayne@68: monthname = [None, jpayne@68: 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', jpayne@68: 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] jpayne@68: jpayne@68: def address_string(self): jpayne@68: """Return the client address.""" jpayne@68: jpayne@68: return self.client_address[0] jpayne@68: jpayne@68: # Essentially static class variables jpayne@68: jpayne@68: # The version of the HTTP protocol we support. jpayne@68: # Set this to HTTP/1.1 to enable automatic keepalive jpayne@68: protocol_version = "HTTP/1.0" jpayne@68: jpayne@68: # MessageClass used to parse headers jpayne@68: MessageClass = http.client.HTTPMessage jpayne@68: jpayne@68: # hack to maintain backwards compatibility jpayne@68: responses = { jpayne@68: v: (v.phrase, v.description) jpayne@68: for v in HTTPStatus.__members__.values() jpayne@68: } jpayne@68: jpayne@68: jpayne@68: class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): jpayne@68: jpayne@68: """Simple HTTP request handler with GET and HEAD commands. jpayne@68: jpayne@68: This serves files from the current directory and any of its jpayne@68: subdirectories. The MIME type for files is determined by jpayne@68: calling the .guess_type() method. jpayne@68: jpayne@68: The GET and HEAD requests are identical except that the HEAD jpayne@68: request omits the actual contents of the file. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: server_version = "SimpleHTTP/" + __version__ jpayne@68: jpayne@68: def __init__(self, *args, directory=None, **kwargs): jpayne@68: if directory is None: jpayne@68: directory = os.getcwd() jpayne@68: self.directory = directory jpayne@68: super().__init__(*args, **kwargs) jpayne@68: jpayne@68: def do_GET(self): jpayne@68: """Serve a GET request.""" jpayne@68: f = self.send_head() jpayne@68: if f: jpayne@68: try: jpayne@68: self.copyfile(f, self.wfile) jpayne@68: finally: jpayne@68: f.close() jpayne@68: jpayne@68: def do_HEAD(self): jpayne@68: """Serve a HEAD request.""" jpayne@68: f = self.send_head() jpayne@68: if f: jpayne@68: f.close() jpayne@68: jpayne@68: def send_head(self): jpayne@68: """Common code for GET and HEAD commands. jpayne@68: jpayne@68: This sends the response code and MIME headers. jpayne@68: jpayne@68: Return value is either a file object (which has to be copied jpayne@68: to the outputfile by the caller unless the command was HEAD, jpayne@68: and must be closed by the caller under all circumstances), or jpayne@68: None, in which case the caller has nothing further to do. jpayne@68: jpayne@68: """ jpayne@68: path = self.translate_path(self.path) jpayne@68: f = None jpayne@68: if os.path.isdir(path): jpayne@68: parts = urllib.parse.urlsplit(self.path) jpayne@68: if not parts.path.endswith('/'): jpayne@68: # redirect browser - doing basically what apache does jpayne@68: self.send_response(HTTPStatus.MOVED_PERMANENTLY) jpayne@68: new_parts = (parts[0], parts[1], parts[2] + '/', jpayne@68: parts[3], parts[4]) jpayne@68: new_url = urllib.parse.urlunsplit(new_parts) jpayne@68: self.send_header("Location", new_url) jpayne@68: self.end_headers() jpayne@68: return None jpayne@68: for index in "index.html", "index.htm": jpayne@68: index = os.path.join(path, index) jpayne@68: if os.path.exists(index): jpayne@68: path = index jpayne@68: break jpayne@68: else: jpayne@68: return self.list_directory(path) jpayne@68: ctype = self.guess_type(path) jpayne@68: # check for trailing "/" which should return 404. See Issue17324 jpayne@68: # The test for this was added in test_httpserver.py jpayne@68: # However, some OS platforms accept a trailingSlash as a filename jpayne@68: # See discussion on python-dev and Issue34711 regarding jpayne@68: # parseing and rejection of filenames with a trailing slash jpayne@68: if path.endswith("/"): jpayne@68: self.send_error(HTTPStatus.NOT_FOUND, "File not found") jpayne@68: return None jpayne@68: try: jpayne@68: f = open(path, 'rb') jpayne@68: except OSError: jpayne@68: self.send_error(HTTPStatus.NOT_FOUND, "File not found") jpayne@68: return None jpayne@68: jpayne@68: try: jpayne@68: fs = os.fstat(f.fileno()) jpayne@68: # Use browser cache if possible jpayne@68: if ("If-Modified-Since" in self.headers jpayne@68: and "If-None-Match" not in self.headers): jpayne@68: # compare If-Modified-Since and time of last file modification jpayne@68: try: jpayne@68: ims = email.utils.parsedate_to_datetime( jpayne@68: self.headers["If-Modified-Since"]) jpayne@68: except (TypeError, IndexError, OverflowError, ValueError): jpayne@68: # ignore ill-formed values jpayne@68: pass jpayne@68: else: jpayne@68: if ims.tzinfo is None: jpayne@68: # obsolete format with no timezone, cf. jpayne@68: # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 jpayne@68: ims = ims.replace(tzinfo=datetime.timezone.utc) jpayne@68: if ims.tzinfo is datetime.timezone.utc: jpayne@68: # compare to UTC datetime of last modification jpayne@68: last_modif = datetime.datetime.fromtimestamp( jpayne@68: fs.st_mtime, datetime.timezone.utc) jpayne@68: # remove microseconds, like in If-Modified-Since jpayne@68: last_modif = last_modif.replace(microsecond=0) jpayne@68: jpayne@68: if last_modif <= ims: jpayne@68: self.send_response(HTTPStatus.NOT_MODIFIED) jpayne@68: self.end_headers() jpayne@68: f.close() jpayne@68: return None jpayne@68: jpayne@68: self.send_response(HTTPStatus.OK) jpayne@68: self.send_header("Content-type", ctype) jpayne@68: self.send_header("Content-Length", str(fs[6])) jpayne@68: self.send_header("Last-Modified", jpayne@68: self.date_time_string(fs.st_mtime)) jpayne@68: self.end_headers() jpayne@68: return f jpayne@68: except: jpayne@68: f.close() jpayne@68: raise jpayne@68: jpayne@68: def list_directory(self, path): jpayne@68: """Helper to produce a directory listing (absent index.html). jpayne@68: jpayne@68: Return value is either a file object, or None (indicating an jpayne@68: error). In either case, the headers are sent, making the jpayne@68: interface the same as for send_head(). jpayne@68: jpayne@68: """ jpayne@68: try: jpayne@68: list = os.listdir(path) jpayne@68: except OSError: jpayne@68: self.send_error( jpayne@68: HTTPStatus.NOT_FOUND, jpayne@68: "No permission to list directory") jpayne@68: return None jpayne@68: list.sort(key=lambda a: a.lower()) jpayne@68: r = [] jpayne@68: try: jpayne@68: displaypath = urllib.parse.unquote(self.path, jpayne@68: errors='surrogatepass') jpayne@68: except UnicodeDecodeError: jpayne@68: displaypath = urllib.parse.unquote(path) jpayne@68: displaypath = html.escape(displaypath, quote=False) jpayne@68: enc = sys.getfilesystemencoding() jpayne@68: title = 'Directory listing for %s' % displaypath jpayne@68: r.append('') jpayne@68: r.append('\n') jpayne@68: r.append('' % enc) jpayne@68: r.append('%s\n' % title) jpayne@68: r.append('\n

%s

' % title) jpayne@68: r.append('
\n
    ') jpayne@68: for name in list: jpayne@68: fullname = os.path.join(path, name) jpayne@68: displayname = linkname = name jpayne@68: # Append / for directories or @ for symbolic links jpayne@68: if os.path.isdir(fullname): jpayne@68: displayname = name + "/" jpayne@68: linkname = name + "/" jpayne@68: if os.path.islink(fullname): jpayne@68: displayname = name + "@" jpayne@68: # Note: a link to a directory displays with @ and links with / jpayne@68: r.append('
  • %s
  • ' jpayne@68: % (urllib.parse.quote(linkname, jpayne@68: errors='surrogatepass'), jpayne@68: html.escape(displayname, quote=False))) jpayne@68: r.append('
\n
\n\n\n') jpayne@68: encoded = '\n'.join(r).encode(enc, 'surrogateescape') jpayne@68: f = io.BytesIO() jpayne@68: f.write(encoded) jpayne@68: f.seek(0) jpayne@68: self.send_response(HTTPStatus.OK) jpayne@68: self.send_header("Content-type", "text/html; charset=%s" % enc) jpayne@68: self.send_header("Content-Length", str(len(encoded))) jpayne@68: self.end_headers() jpayne@68: return f jpayne@68: jpayne@68: def translate_path(self, path): jpayne@68: """Translate a /-separated PATH to the local filename syntax. jpayne@68: jpayne@68: Components that mean special things to the local file system jpayne@68: (e.g. drive or directory names) are ignored. (XXX They should jpayne@68: probably be diagnosed.) jpayne@68: jpayne@68: """ jpayne@68: # abandon query parameters jpayne@68: path = path.split('?',1)[0] jpayne@68: path = path.split('#',1)[0] jpayne@68: # Don't forget explicit trailing slash when normalizing. Issue17324 jpayne@68: trailing_slash = path.rstrip().endswith('/') jpayne@68: try: jpayne@68: path = urllib.parse.unquote(path, errors='surrogatepass') jpayne@68: except UnicodeDecodeError: jpayne@68: path = urllib.parse.unquote(path) jpayne@68: path = posixpath.normpath(path) jpayne@68: words = path.split('/') jpayne@68: words = filter(None, words) jpayne@68: path = self.directory jpayne@68: for word in words: jpayne@68: if os.path.dirname(word) or word in (os.curdir, os.pardir): jpayne@68: # Ignore components that are not a simple file/directory name jpayne@68: continue jpayne@68: path = os.path.join(path, word) jpayne@68: if trailing_slash: jpayne@68: path += '/' jpayne@68: return path jpayne@68: jpayne@68: def copyfile(self, source, outputfile): jpayne@68: """Copy all data between two file objects. jpayne@68: jpayne@68: The SOURCE argument is a file object open for reading jpayne@68: (or anything with a read() method) and the DESTINATION jpayne@68: argument is a file object open for writing (or jpayne@68: anything with a write() method). jpayne@68: jpayne@68: The only reason for overriding this would be to change jpayne@68: the block size or perhaps to replace newlines by CRLF jpayne@68: -- note however that this the default server uses this jpayne@68: to copy binary data as well. jpayne@68: jpayne@68: """ jpayne@68: shutil.copyfileobj(source, outputfile) jpayne@68: jpayne@68: def guess_type(self, path): jpayne@68: """Guess the type of a file. jpayne@68: jpayne@68: Argument is a PATH (a filename). jpayne@68: jpayne@68: Return value is a string of the form type/subtype, jpayne@68: usable for a MIME Content-type header. jpayne@68: jpayne@68: The default implementation looks the file's extension jpayne@68: up in the table self.extensions_map, using application/octet-stream jpayne@68: as a default; however it would be permissible (if jpayne@68: slow) to look inside the data to make a better guess. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: base, ext = posixpath.splitext(path) jpayne@68: if ext in self.extensions_map: jpayne@68: return self.extensions_map[ext] jpayne@68: ext = ext.lower() jpayne@68: if ext in self.extensions_map: jpayne@68: return self.extensions_map[ext] jpayne@68: else: jpayne@68: return self.extensions_map[''] jpayne@68: jpayne@68: if not mimetypes.inited: jpayne@68: mimetypes.init() # try to read system mime.types jpayne@68: extensions_map = mimetypes.types_map.copy() jpayne@68: extensions_map.update({ jpayne@68: '': 'application/octet-stream', # Default jpayne@68: '.py': 'text/plain', jpayne@68: '.c': 'text/plain', jpayne@68: '.h': 'text/plain', jpayne@68: }) jpayne@68: jpayne@68: jpayne@68: # Utilities for CGIHTTPRequestHandler jpayne@68: jpayne@68: def _url_collapse_path(path): jpayne@68: """ jpayne@68: Given a URL path, remove extra '/'s and '.' path elements and collapse jpayne@68: any '..' references and returns a collapsed path. jpayne@68: jpayne@68: Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. jpayne@68: The utility of this function is limited to is_cgi method and helps jpayne@68: preventing some security attacks. jpayne@68: jpayne@68: Returns: The reconstituted URL, which will always start with a '/'. jpayne@68: jpayne@68: Raises: IndexError if too many '..' occur within the path. jpayne@68: jpayne@68: """ jpayne@68: # Query component should not be involved. jpayne@68: path, _, query = path.partition('?') jpayne@68: path = urllib.parse.unquote(path) jpayne@68: jpayne@68: # Similar to os.path.split(os.path.normpath(path)) but specific to URL jpayne@68: # path semantics rather than local operating system semantics. jpayne@68: path_parts = path.split('/') jpayne@68: head_parts = [] jpayne@68: for part in path_parts[:-1]: jpayne@68: if part == '..': jpayne@68: head_parts.pop() # IndexError if more '..' than prior parts jpayne@68: elif part and part != '.': jpayne@68: head_parts.append( part ) jpayne@68: if path_parts: jpayne@68: tail_part = path_parts.pop() jpayne@68: if tail_part: jpayne@68: if tail_part == '..': jpayne@68: head_parts.pop() jpayne@68: tail_part = '' jpayne@68: elif tail_part == '.': jpayne@68: tail_part = '' jpayne@68: else: jpayne@68: tail_part = '' jpayne@68: jpayne@68: if query: jpayne@68: tail_part = '?'.join((tail_part, query)) jpayne@68: jpayne@68: splitpath = ('/' + '/'.join(head_parts), tail_part) jpayne@68: collapsed_path = "/".join(splitpath) jpayne@68: jpayne@68: return collapsed_path jpayne@68: jpayne@68: jpayne@68: jpayne@68: nobody = None jpayne@68: jpayne@68: def nobody_uid(): jpayne@68: """Internal routine to get nobody's uid""" jpayne@68: global nobody jpayne@68: if nobody: jpayne@68: return nobody jpayne@68: try: jpayne@68: import pwd jpayne@68: except ImportError: jpayne@68: return -1 jpayne@68: try: jpayne@68: nobody = pwd.getpwnam('nobody')[2] jpayne@68: except KeyError: jpayne@68: nobody = 1 + max(x[2] for x in pwd.getpwall()) jpayne@68: return nobody jpayne@68: jpayne@68: jpayne@68: def executable(path): jpayne@68: """Test for executable file.""" jpayne@68: return os.access(path, os.X_OK) jpayne@68: jpayne@68: jpayne@68: class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): jpayne@68: jpayne@68: """Complete HTTP server with GET, HEAD and POST commands. jpayne@68: jpayne@68: GET and HEAD also support running CGI scripts. jpayne@68: jpayne@68: The POST command is *only* implemented for CGI scripts. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: # Determine platform specifics jpayne@68: have_fork = hasattr(os, 'fork') jpayne@68: jpayne@68: # Make rfile unbuffered -- we need to read one line and then pass jpayne@68: # the rest to a subprocess, so we can't use buffered input. jpayne@68: rbufsize = 0 jpayne@68: jpayne@68: def do_POST(self): jpayne@68: """Serve a POST request. jpayne@68: jpayne@68: This is only implemented for CGI scripts. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: if self.is_cgi(): jpayne@68: self.run_cgi() jpayne@68: else: jpayne@68: self.send_error( jpayne@68: HTTPStatus.NOT_IMPLEMENTED, jpayne@68: "Can only POST to CGI scripts") jpayne@68: jpayne@68: def send_head(self): jpayne@68: """Version of send_head that support CGI scripts""" jpayne@68: if self.is_cgi(): jpayne@68: return self.run_cgi() jpayne@68: else: jpayne@68: return SimpleHTTPRequestHandler.send_head(self) jpayne@68: jpayne@68: def is_cgi(self): jpayne@68: """Test whether self.path corresponds to a CGI script. jpayne@68: jpayne@68: Returns True and updates the cgi_info attribute to the tuple jpayne@68: (dir, rest) if self.path requires running a CGI script. jpayne@68: Returns False otherwise. jpayne@68: jpayne@68: If any exception is raised, the caller should assume that jpayne@68: self.path was rejected as invalid and act accordingly. jpayne@68: jpayne@68: The default implementation tests whether the normalized url jpayne@68: path begins with one of the strings in self.cgi_directories jpayne@68: (and the next character is a '/' or the end of the string). jpayne@68: jpayne@68: """ jpayne@68: collapsed_path = _url_collapse_path(self.path) jpayne@68: dir_sep = collapsed_path.find('/', 1) jpayne@68: head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] jpayne@68: if head in self.cgi_directories: jpayne@68: self.cgi_info = head, tail jpayne@68: return True jpayne@68: return False jpayne@68: jpayne@68: jpayne@68: cgi_directories = ['/cgi-bin', '/htbin'] jpayne@68: jpayne@68: def is_executable(self, path): jpayne@68: """Test whether argument path is an executable file.""" jpayne@68: return executable(path) jpayne@68: jpayne@68: def is_python(self, path): jpayne@68: """Test whether argument path is a Python script.""" jpayne@68: head, tail = os.path.splitext(path) jpayne@68: return tail.lower() in (".py", ".pyw") jpayne@68: jpayne@68: def run_cgi(self): jpayne@68: """Execute a CGI script.""" jpayne@68: dir, rest = self.cgi_info jpayne@68: path = dir + '/' + rest jpayne@68: i = path.find('/', len(dir)+1) jpayne@68: while i >= 0: jpayne@68: nextdir = path[:i] jpayne@68: nextrest = path[i+1:] jpayne@68: jpayne@68: scriptdir = self.translate_path(nextdir) jpayne@68: if os.path.isdir(scriptdir): jpayne@68: dir, rest = nextdir, nextrest jpayne@68: i = path.find('/', len(dir)+1) jpayne@68: else: jpayne@68: break jpayne@68: jpayne@68: # find an explicit query string, if present. jpayne@68: rest, _, query = rest.partition('?') jpayne@68: jpayne@68: # dissect the part after the directory name into a script name & jpayne@68: # a possible additional path, to be stored in PATH_INFO. jpayne@68: i = rest.find('/') jpayne@68: if i >= 0: jpayne@68: script, rest = rest[:i], rest[i:] jpayne@68: else: jpayne@68: script, rest = rest, '' jpayne@68: jpayne@68: scriptname = dir + '/' + script jpayne@68: scriptfile = self.translate_path(scriptname) jpayne@68: if not os.path.exists(scriptfile): jpayne@68: self.send_error( jpayne@68: HTTPStatus.NOT_FOUND, jpayne@68: "No such CGI script (%r)" % scriptname) jpayne@68: return jpayne@68: if not os.path.isfile(scriptfile): jpayne@68: self.send_error( jpayne@68: HTTPStatus.FORBIDDEN, jpayne@68: "CGI script is not a plain file (%r)" % scriptname) jpayne@68: return jpayne@68: ispy = self.is_python(scriptname) jpayne@68: if self.have_fork or not ispy: jpayne@68: if not self.is_executable(scriptfile): jpayne@68: self.send_error( jpayne@68: HTTPStatus.FORBIDDEN, jpayne@68: "CGI script is not executable (%r)" % scriptname) jpayne@68: return jpayne@68: jpayne@68: # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html jpayne@68: # XXX Much of the following could be prepared ahead of time! jpayne@68: env = copy.deepcopy(os.environ) jpayne@68: env['SERVER_SOFTWARE'] = self.version_string() jpayne@68: env['SERVER_NAME'] = self.server.server_name jpayne@68: env['GATEWAY_INTERFACE'] = 'CGI/1.1' jpayne@68: env['SERVER_PROTOCOL'] = self.protocol_version jpayne@68: env['SERVER_PORT'] = str(self.server.server_port) jpayne@68: env['REQUEST_METHOD'] = self.command jpayne@68: uqrest = urllib.parse.unquote(rest) jpayne@68: env['PATH_INFO'] = uqrest jpayne@68: env['PATH_TRANSLATED'] = self.translate_path(uqrest) jpayne@68: env['SCRIPT_NAME'] = scriptname jpayne@68: if query: jpayne@68: env['QUERY_STRING'] = query jpayne@68: env['REMOTE_ADDR'] = self.client_address[0] jpayne@68: authorization = self.headers.get("authorization") jpayne@68: if authorization: jpayne@68: authorization = authorization.split() jpayne@68: if len(authorization) == 2: jpayne@68: import base64, binascii jpayne@68: env['AUTH_TYPE'] = authorization[0] jpayne@68: if authorization[0].lower() == "basic": jpayne@68: try: jpayne@68: authorization = authorization[1].encode('ascii') jpayne@68: authorization = base64.decodebytes(authorization).\ jpayne@68: decode('ascii') jpayne@68: except (binascii.Error, UnicodeError): jpayne@68: pass jpayne@68: else: jpayne@68: authorization = authorization.split(':') jpayne@68: if len(authorization) == 2: jpayne@68: env['REMOTE_USER'] = authorization[0] jpayne@68: # XXX REMOTE_IDENT jpayne@68: if self.headers.get('content-type') is None: jpayne@68: env['CONTENT_TYPE'] = self.headers.get_content_type() jpayne@68: else: jpayne@68: env['CONTENT_TYPE'] = self.headers['content-type'] jpayne@68: length = self.headers.get('content-length') jpayne@68: if length: jpayne@68: env['CONTENT_LENGTH'] = length jpayne@68: referer = self.headers.get('referer') jpayne@68: if referer: jpayne@68: env['HTTP_REFERER'] = referer jpayne@68: accept = [] jpayne@68: for line in self.headers.getallmatchingheaders('accept'): jpayne@68: if line[:1] in "\t\n\r ": jpayne@68: accept.append(line.strip()) jpayne@68: else: jpayne@68: accept = accept + line[7:].split(',') jpayne@68: env['HTTP_ACCEPT'] = ','.join(accept) jpayne@68: ua = self.headers.get('user-agent') jpayne@68: if ua: jpayne@68: env['HTTP_USER_AGENT'] = ua jpayne@68: co = filter(None, self.headers.get_all('cookie', [])) jpayne@68: cookie_str = ', '.join(co) jpayne@68: if cookie_str: jpayne@68: env['HTTP_COOKIE'] = cookie_str jpayne@68: # XXX Other HTTP_* headers jpayne@68: # Since we're setting the env in the parent, provide empty jpayne@68: # values to override previously set values jpayne@68: for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', jpayne@68: 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): jpayne@68: env.setdefault(k, "") jpayne@68: jpayne@68: self.send_response(HTTPStatus.OK, "Script output follows") jpayne@68: self.flush_headers() jpayne@68: jpayne@68: decoded_query = query.replace('+', ' ') jpayne@68: jpayne@68: if self.have_fork: jpayne@68: # Unix -- fork as we should jpayne@68: args = [script] jpayne@68: if '=' not in decoded_query: jpayne@68: args.append(decoded_query) jpayne@68: nobody = nobody_uid() jpayne@68: self.wfile.flush() # Always flush before forking jpayne@68: pid = os.fork() jpayne@68: if pid != 0: jpayne@68: # Parent jpayne@68: pid, sts = os.waitpid(pid, 0) jpayne@68: # throw away additional data [see bug #427345] jpayne@68: while select.select([self.rfile], [], [], 0)[0]: jpayne@68: if not self.rfile.read(1): jpayne@68: break jpayne@68: if sts: jpayne@68: self.log_error("CGI script exit status %#x", sts) jpayne@68: return jpayne@68: # Child jpayne@68: try: jpayne@68: try: jpayne@68: os.setuid(nobody) jpayne@68: except OSError: jpayne@68: pass jpayne@68: os.dup2(self.rfile.fileno(), 0) jpayne@68: os.dup2(self.wfile.fileno(), 1) jpayne@68: os.execve(scriptfile, args, env) jpayne@68: except: jpayne@68: self.server.handle_error(self.request, self.client_address) jpayne@68: os._exit(127) jpayne@68: jpayne@68: else: jpayne@68: # Non-Unix -- use subprocess jpayne@68: import subprocess jpayne@68: cmdline = [scriptfile] jpayne@68: if self.is_python(scriptfile): jpayne@68: interp = sys.executable jpayne@68: if interp.lower().endswith("w.exe"): jpayne@68: # On Windows, use python.exe, not pythonw.exe jpayne@68: interp = interp[:-5] + interp[-4:] jpayne@68: cmdline = [interp, '-u'] + cmdline jpayne@68: if '=' not in query: jpayne@68: cmdline.append(query) jpayne@68: self.log_message("command: %s", subprocess.list2cmdline(cmdline)) jpayne@68: try: jpayne@68: nbytes = int(length) jpayne@68: except (TypeError, ValueError): jpayne@68: nbytes = 0 jpayne@68: p = subprocess.Popen(cmdline, jpayne@68: stdin=subprocess.PIPE, jpayne@68: stdout=subprocess.PIPE, jpayne@68: stderr=subprocess.PIPE, jpayne@68: env = env jpayne@68: ) jpayne@68: if self.command.lower() == "post" and nbytes > 0: jpayne@68: data = self.rfile.read(nbytes) jpayne@68: else: jpayne@68: data = None jpayne@68: # throw away additional data [see bug #427345] jpayne@68: while select.select([self.rfile._sock], [], [], 0)[0]: jpayne@68: if not self.rfile._sock.recv(1): jpayne@68: break jpayne@68: stdout, stderr = p.communicate(data) jpayne@68: self.wfile.write(stdout) jpayne@68: if stderr: jpayne@68: self.log_error('%s', stderr) jpayne@68: p.stderr.close() jpayne@68: p.stdout.close() jpayne@68: status = p.returncode jpayne@68: if status: jpayne@68: self.log_error("CGI script exit status %#x", status) jpayne@68: else: jpayne@68: self.log_message("CGI script exited OK") jpayne@68: jpayne@68: jpayne@68: def _get_best_family(*address): jpayne@68: infos = socket.getaddrinfo( jpayne@68: *address, jpayne@68: type=socket.SOCK_STREAM, jpayne@68: flags=socket.AI_PASSIVE, jpayne@68: ) jpayne@68: family, type, proto, canonname, sockaddr = next(iter(infos)) jpayne@68: return family, sockaddr jpayne@68: jpayne@68: jpayne@68: def test(HandlerClass=BaseHTTPRequestHandler, jpayne@68: ServerClass=ThreadingHTTPServer, jpayne@68: protocol="HTTP/1.0", port=8000, bind=None): jpayne@68: """Test the HTTP request handler class. jpayne@68: jpayne@68: This runs an HTTP server on port 8000 (or the port argument). jpayne@68: jpayne@68: """ jpayne@68: ServerClass.address_family, addr = _get_best_family(bind, port) jpayne@68: jpayne@68: HandlerClass.protocol_version = protocol jpayne@68: with ServerClass(addr, HandlerClass) as httpd: jpayne@68: host, port = httpd.socket.getsockname()[:2] jpayne@68: url_host = f'[{host}]' if ':' in host else host jpayne@68: print( jpayne@68: f"Serving HTTP on {host} port {port} " jpayne@68: f"(http://{url_host}:{port}/) ..." jpayne@68: ) jpayne@68: try: jpayne@68: httpd.serve_forever() jpayne@68: except KeyboardInterrupt: jpayne@68: print("\nKeyboard interrupt received, exiting.") jpayne@68: sys.exit(0) jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: import argparse jpayne@68: jpayne@68: parser = argparse.ArgumentParser() jpayne@68: parser.add_argument('--cgi', action='store_true', jpayne@68: help='Run as CGI Server') jpayne@68: parser.add_argument('--bind', '-b', metavar='ADDRESS', jpayne@68: help='Specify alternate bind address ' jpayne@68: '[default: all interfaces]') jpayne@68: parser.add_argument('--directory', '-d', default=os.getcwd(), jpayne@68: help='Specify alternative directory ' jpayne@68: '[default:current directory]') jpayne@68: parser.add_argument('port', action='store', jpayne@68: default=8000, type=int, jpayne@68: nargs='?', jpayne@68: help='Specify alternate port [default: 8000]') jpayne@68: args = parser.parse_args() jpayne@68: if args.cgi: jpayne@68: handler_class = CGIHTTPRequestHandler jpayne@68: else: jpayne@68: handler_class = partial(SimpleHTTPRequestHandler, jpayne@68: directory=args.directory) jpayne@68: test(HandlerClass=handler_class, port=args.port, bind=args.bind)