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

Error response

jpayne@69:

Error code: %(code)d

jpayne@69:

Message: %(message)s.

jpayne@69:

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

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

%s

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