ppt_control / http_server_39.pyon commit replace client-triggered state refresh with server-triggered (d75ca91)
   1"""HTTP server classes.
   2
   3Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
   4SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST,
   5and CGIHTTPRequestHandler for CGI scripts.
   6
   7It does, however, optionally implement HTTP/1.1 persistent connections,
   8as of version 0.3.
   9
  10Notes on CGIHTTPRequestHandler
  11------------------------------
  12
  13This class implements GET and POST requests to cgi-bin scripts.
  14
  15If the os.fork() function is not present (e.g. on Windows),
  16subprocess.Popen() is used as a fallback, with slightly altered semantics.
  17
  18In all cases, the implementation is intentionally naive -- all
  19requests are executed synchronously.
  20
  21SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
  22-- it may execute arbitrary Python code or external programs.
  23
  24Note that status code 200 is sent prior to execution of a CGI script, so
  25scripts cannot send other status codes such as 302 (redirect).
  26
  27XXX To do:
  28
  29- log requests even later (to capture byte count)
  30- log user-agent header and other interesting goodies
  31- send error log to separate file
  32"""
  33
  34
  35# See also:
  36#
  37# HTTP Working Group                                        T. Berners-Lee
  38# INTERNET-DRAFT                                            R. T. Fielding
  39# <draft-ietf-http-v10-spec-00.txt>                     H. Frystyk Nielsen
  40# Expires September 8, 1995                                  March 8, 1995
  41#
  42# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
  43#
  44# and
  45#
  46# Network Working Group                                      R. Fielding
  47# Request for Comments: 2616                                       et al
  48# Obsoletes: 2068                                              June 1999
  49# Category: Standards Track
  50#
  51# URL: http://www.faqs.org/rfcs/rfc2616.html
  52
  53# Log files
  54# ---------
  55#
  56# Here's a quote from the NCSA httpd docs about log file format.
  57#
  58# | The logfile format is as follows. Each line consists of:
  59# |
  60# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb
  61# |
  62# |        host: Either the DNS name or the IP number of the remote client
  63# |        rfc931: Any information returned by identd for this person,
  64# |                - otherwise.
  65# |        authuser: If user sent a userid for authentication, the user name,
  66# |                  - otherwise.
  67# |        DD: Day
  68# |        Mon: Month (calendar name)
  69# |        YYYY: Year
  70# |        hh: hour (24-hour format, the machine's timezone)
  71# |        mm: minutes
  72# |        ss: seconds
  73# |        request: The first line of the HTTP request as sent by the client.
  74# |        ddd: the status code returned by the server, - if not available.
  75# |        bbbb: the total number of bytes sent,
  76# |              *not including the HTTP/1.0 header*, - if not available
  77# |
  78# | You can determine the name of the file accessed through request.
  79#
  80# (Actually, the latter is only true if you know the server configuration
  81# at the time the request was made!)
  82
  83__version__ = "0.6"
  84
  85__all__ = [
  86    "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
  87    "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
  88]
  89
  90import copy
  91import datetime
  92import email.utils
  93import html
  94import http.client
  95import io
  96import mimetypes
  97import os
  98import posixpath
  99import select
 100import shutil
 101import socket # For gethostbyaddr()
 102import socketserver
 103import sys
 104import time
 105import urllib.parse
 106import contextlib
 107from functools import partial
 108
 109from http import HTTPStatus
 110
 111
 112# Default error message template
 113DEFAULT_ERROR_MESSAGE = """\
 114<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
 115        "http://www.w3.org/TR/html4/strict.dtd">
 116<html>
 117    <head>
 118        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
 119        <title>Error response</title>
 120    </head>
 121    <body>
 122        <h1>Error response</h1>
 123        <p>Error code: %(code)d</p>
 124        <p>Message: %(message)s.</p>
 125        <p>Error code explanation: %(code)s - %(explain)s.</p>
 126    </body>
 127</html>
 128"""
 129
 130DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
 131
 132class HTTPServer(socketserver.TCPServer):
 133
 134    allow_reuse_address = 1    # Seems to make sense in testing environment
 135
 136    def server_bind(self):
 137        """Override server_bind to store the server name."""
 138        socketserver.TCPServer.server_bind(self)
 139        host, port = self.server_address[:2]
 140        self.server_name = socket.getfqdn(host)
 141        self.server_port = port
 142
 143
 144class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
 145    daemon_threads = True
 146
 147
 148class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
 149
 150    """HTTP request handler base class.
 151
 152    The following explanation of HTTP serves to guide you through the
 153    code as well as to expose any misunderstandings I may have about
 154    HTTP (so you don't need to read the code to figure out I'm wrong
 155    :-).
 156
 157    HTTP (HyperText Transfer Protocol) is an extensible protocol on
 158    top of a reliable stream transport (e.g. TCP/IP).  The protocol
 159    recognizes three parts to a request:
 160
 161    1. One line identifying the request type and path
 162    2. An optional set of RFC-822-style headers
 163    3. An optional data part
 164
 165    The headers and data are separated by a blank line.
 166
 167    The first line of the request has the form
 168
 169    <command> <path> <version>
 170
 171    where <command> is a (case-sensitive) keyword such as GET or POST,
 172    <path> is a string containing path information for the request,
 173    and <version> should be the string "HTTP/1.0" or "HTTP/1.1".
 174    <path> is encoded using the URL encoding scheme (using %xx to signify
 175    the ASCII character with hex code xx).
 176
 177    The specification specifies that lines are separated by CRLF but
 178    for compatibility with the widest range of clients recommends
 179    servers also handle LF.  Similarly, whitespace in the request line
 180    is treated sensibly (allowing multiple spaces between components
 181    and allowing trailing whitespace).
 182
 183    Similarly, for output, lines ought to be separated by CRLF pairs
 184    but most clients grok LF characters just fine.
 185
 186    If the first line of the request has the form
 187
 188    <command> <path>
 189
 190    (i.e. <version> is left out) then this is assumed to be an HTTP
 191    0.9 request; this form has no optional headers and data part and
 192    the reply consists of just the data.
 193
 194    The reply form of the HTTP 1.x protocol again has three parts:
 195
 196    1. One line giving the response code
 197    2. An optional set of RFC-822-style headers
 198    3. The data
 199
 200    Again, the headers and data are separated by a blank line.
 201
 202    The response code line has the form
 203
 204    <version> <responsecode> <responsestring>
 205
 206    where <version> is the protocol version ("HTTP/1.0" or "HTTP/1.1"),
 207    <responsecode> is a 3-digit response code indicating success or
 208    failure of the request, and <responsestring> is an optional
 209    human-readable string explaining what the response code means.
 210
 211    This server parses the request and the headers, and then calls a
 212    function specific to the request type (<command>).  Specifically,
 213    a request SPAM will be handled by a method do_SPAM().  If no
 214    such method exists the server sends an error response to the
 215    client.  If it exists, it is called with no arguments:
 216
 217    do_SPAM()
 218
 219    Note that the request name is case sensitive (i.e. SPAM and spam
 220    are different requests).
 221
 222    The various request details are stored in instance variables:
 223
 224    - client_address is the client IP address in the form (host,
 225    port);
 226
 227    - command, path and version are the broken-down request line;
 228
 229    - headers is an instance of email.message.Message (or a derived
 230    class) containing the header information;
 231
 232    - rfile is a file object open for reading positioned at the
 233    start of the optional input data part;
 234
 235    - wfile is a file object open for writing.
 236
 237    IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING!
 238
 239    The first thing to be written must be the response line.  Then
 240    follow 0 or more header lines, then a blank line, and then the
 241    actual data (if any).  The meaning of the header lines depends on
 242    the command executed by the server; in most cases, when data is
 243    returned, there should be at least one header line of the form
 244
 245    Content-type: <type>/<subtype>
 246
 247    where <type> and <subtype> should be registered MIME types,
 248    e.g. "text/html" or "text/plain".
 249
 250    """
 251
 252    # The Python system version, truncated to its first component.
 253    sys_version = "Python/" + sys.version.split()[0]
 254
 255    # The server software version.  You may want to override this.
 256    # The format is multiple whitespace-separated strings,
 257    # where each string is of the form name[/version].
 258    server_version = "BaseHTTP/" + __version__
 259
 260    error_message_format = DEFAULT_ERROR_MESSAGE
 261    error_content_type = DEFAULT_ERROR_CONTENT_TYPE
 262
 263    # The default request version.  This only affects responses up until
 264    # the point where the request line is parsed, so it mainly decides what
 265    # the client gets back when sending a malformed request line.
 266    # Most web servers default to HTTP 0.9, i.e. don't send a status line.
 267    default_request_version = "HTTP/0.9"
 268
 269    def parse_request(self):
 270        """Parse a request (internal).
 271
 272        The request should be stored in self.raw_requestline; the results
 273        are in self.command, self.path, self.request_version and
 274        self.headers.
 275
 276        Return True for success, False for failure; on failure, any relevant
 277        error response has already been sent back.
 278
 279        """
 280        self.command = None  # set in case of error on the first line
 281        self.request_version = version = self.default_request_version
 282        self.close_connection = True
 283        requestline = str(self.raw_requestline, 'iso-8859-1')
 284        requestline = requestline.rstrip('\r\n')
 285        self.requestline = requestline
 286        words = requestline.split()
 287        if len(words) == 0:
 288            return False
 289
 290        if len(words) >= 3:  # Enough to determine protocol version
 291            version = words[-1]
 292            try:
 293                if not version.startswith('HTTP/'):
 294                    raise ValueError
 295                base_version_number = version.split('/', 1)[1]
 296                version_number = base_version_number.split(".")
 297                # RFC 2145 section 3.1 says there can be only one "." and
 298                #   - major and minor numbers MUST be treated as
 299                #      separate integers;
 300                #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
 301                #      turn is lower than HTTP/12.3;
 302                #   - Leading zeros MUST be ignored by recipients.
 303                if len(version_number) != 2:
 304                    raise ValueError
 305                version_number = int(version_number[0]), int(version_number[1])
 306            except (ValueError, IndexError):
 307                self.send_error(
 308                    HTTPStatus.BAD_REQUEST,
 309                    "Bad request version (%r)" % version)
 310                return False
 311            if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
 312                self.close_connection = False
 313            if version_number >= (2, 0):
 314                self.send_error(
 315                    HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
 316                    "Invalid HTTP version (%s)" % base_version_number)
 317                return False
 318            self.request_version = version
 319
 320        if not 2 <= len(words) <= 3:
 321            self.send_error(
 322                HTTPStatus.BAD_REQUEST,
 323                "Bad request syntax (%r)" % requestline)
 324            return False
 325        command, path = words[:2]
 326        if len(words) == 2:
 327            self.close_connection = True
 328            if command != 'GET':
 329                self.send_error(
 330                    HTTPStatus.BAD_REQUEST,
 331                    "Bad HTTP/0.9 request type (%r)" % command)
 332                return False
 333        self.command, self.path = command, path
 334
 335        # Examine the headers and look for a Connection directive.
 336        try:
 337            self.headers = http.client.parse_headers(self.rfile,
 338                                                     _class=self.MessageClass)
 339        except http.client.LineTooLong as err:
 340            self.send_error(
 341                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
 342                "Line too long",
 343                str(err))
 344            return False
 345        except http.client.HTTPException as err:
 346            self.send_error(
 347                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
 348                "Too many headers",
 349                str(err)
 350            )
 351            return False
 352
 353        conntype = self.headers.get('Connection', "")
 354        if conntype.lower() == 'close':
 355            self.close_connection = True
 356        elif (conntype.lower() == 'keep-alive' and
 357              self.protocol_version >= "HTTP/1.1"):
 358            self.close_connection = False
 359        # Examine the headers and look for an Expect directive
 360        expect = self.headers.get('Expect', "")
 361        if (expect.lower() == "100-continue" and
 362                self.protocol_version >= "HTTP/1.1" and
 363                self.request_version >= "HTTP/1.1"):
 364            if not self.handle_expect_100():
 365                return False
 366        return True
 367
 368    def handle_expect_100(self):
 369        """Decide what to do with an "Expect: 100-continue" header.
 370
 371        If the client is expecting a 100 Continue response, we must
 372        respond with either a 100 Continue or a final response before
 373        waiting for the request body. The default is to always respond
 374        with a 100 Continue. You can behave differently (for example,
 375        reject unauthorized requests) by overriding this method.
 376
 377        This method should either return True (possibly after sending
 378        a 100 Continue response) or send an error response and return
 379        False.
 380
 381        """
 382        self.send_response_only(HTTPStatus.CONTINUE)
 383        self.end_headers()
 384        return True
 385
 386    def handle_one_request(self):
 387        """Handle a single HTTP request.
 388
 389        You normally don't need to override this method; see the class
 390        __doc__ string for information on how to handle specific HTTP
 391        commands such as GET and POST.
 392
 393        """
 394        try:
 395            self.raw_requestline = self.rfile.readline(65537)
 396            if len(self.raw_requestline) > 65536:
 397                self.requestline = ''
 398                self.request_version = ''
 399                self.command = ''
 400                self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
 401                return
 402            if not self.raw_requestline:
 403                self.close_connection = True
 404                return
 405            if not self.parse_request():
 406                # An error code has been sent, just exit
 407                return
 408            mname = 'do_' + self.command
 409            if not hasattr(self, mname):
 410                self.send_error(
 411                    HTTPStatus.NOT_IMPLEMENTED,
 412                    "Unsupported method (%r)" % self.command)
 413                return
 414            method = getattr(self, mname)
 415            method()
 416            self.wfile.flush() #actually send the response if not already done.
 417        except socket.timeout as e:
 418            #a read or a write timed out.  Discard this connection
 419            self.log_error("Request timed out: %r", e)
 420            self.close_connection = True
 421            return
 422
 423    def handle(self):
 424        """Handle multiple requests if necessary."""
 425        self.close_connection = True
 426
 427        self.handle_one_request()
 428        while not self.close_connection:
 429            self.handle_one_request()
 430
 431    def send_error(self, code, message=None, explain=None):
 432        """Send and log an error reply.
 433
 434        Arguments are
 435        * code:    an HTTP error code
 436                   3 digits
 437        * message: a simple optional 1 line reason phrase.
 438                   *( HTAB / SP / VCHAR / %x80-FF )
 439                   defaults to short entry matching the response code
 440        * explain: a detailed message defaults to the long entry
 441                   matching the response code.
 442
 443        This sends an error response (so it must be called before any
 444        output has been generated), logs the error, and finally sends
 445        a piece of HTML explaining the error to the user.
 446
 447        """
 448
 449        try:
 450            shortmsg, longmsg = self.responses[code]
 451        except KeyError:
 452            shortmsg, longmsg = '???', '???'
 453        if message is None:
 454            message = shortmsg
 455        if explain is None:
 456            explain = longmsg
 457        self.log_error("code %d, message %s", code, message)
 458        self.send_response(code, message)
 459        self.send_header('Connection', 'close')
 460
 461        # Message body is omitted for cases described in:
 462        #  - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
 463        #  - RFC7231: 6.3.6. 205(Reset Content)
 464        body = None
 465        if (code >= 200 and
 466            code not in (HTTPStatus.NO_CONTENT,
 467                         HTTPStatus.RESET_CONTENT,
 468                         HTTPStatus.NOT_MODIFIED)):
 469            # HTML encode to prevent Cross Site Scripting attacks
 470            # (see bug #1100201)
 471            content = (self.error_message_format % {
 472                'code': code,
 473                'message': html.escape(message, quote=False),
 474                'explain': html.escape(explain, quote=False)
 475            })
 476            body = content.encode('UTF-8', 'replace')
 477            self.send_header("Content-Type", self.error_content_type)
 478            self.send_header('Content-Length', str(len(body)))
 479        self.end_headers()
 480
 481        if self.command != 'HEAD' and body:
 482            self.wfile.write(body)
 483
 484    def send_response(self, code, message=None):
 485        """Add the response header to the headers buffer and log the
 486        response code.
 487
 488        Also send two standard headers with the server software
 489        version and the current date.
 490
 491        """
 492        self.log_request(code)
 493        self.send_response_only(code, message)
 494        self.send_header('Server', self.version_string())
 495        self.send_header('Date', self.date_time_string())
 496
 497    def send_response_only(self, code, message=None):
 498        """Send the response header only."""
 499        if self.request_version != 'HTTP/0.9':
 500            if message is None:
 501                if code in self.responses:
 502                    message = self.responses[code][0]
 503                else:
 504                    message = ''
 505            if not hasattr(self, '_headers_buffer'):
 506                self._headers_buffer = []
 507            self._headers_buffer.append(("%s %d %s\r\n" %
 508                    (self.protocol_version, code, message)).encode(
 509                        'latin-1', 'strict'))
 510
 511    def send_header(self, keyword, value):
 512        """Send a MIME header to the headers buffer."""
 513        if self.request_version != 'HTTP/0.9':
 514            if not hasattr(self, '_headers_buffer'):
 515                self._headers_buffer = []
 516            self._headers_buffer.append(
 517                ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
 518
 519        if keyword.lower() == 'connection':
 520            if value.lower() == 'close':
 521                self.close_connection = True
 522            elif value.lower() == 'keep-alive':
 523                self.close_connection = False
 524
 525    def end_headers(self):
 526        """Send the blank line ending the MIME headers."""
 527        if self.request_version != 'HTTP/0.9':
 528            self._headers_buffer.append(b"\r\n")
 529            self.flush_headers()
 530
 531    def flush_headers(self):
 532        if hasattr(self, '_headers_buffer'):
 533            self.wfile.write(b"".join(self._headers_buffer))
 534            self._headers_buffer = []
 535
 536    def log_request(self, code='-', size='-'):
 537        """Log an accepted request.
 538
 539        This is called by send_response().
 540
 541        """
 542        if isinstance(code, HTTPStatus):
 543            code = code.value
 544        self.log_message('"%s" %s %s',
 545                         self.requestline, str(code), str(size))
 546
 547    def log_error(self, format, *args):
 548        """Log an error.
 549
 550        This is called when a request cannot be fulfilled.  By
 551        default it passes the message on to log_message().
 552
 553        Arguments are the same as for log_message().
 554
 555        XXX This should go to the separate error log.
 556
 557        """
 558
 559        self.log_message(format, *args)
 560
 561    def log_message(self, format, *args):
 562        """Log an arbitrary message.
 563
 564        This is used by all other logging functions.  Override
 565        it if you have specific logging wishes.
 566
 567        The first argument, FORMAT, is a format string for the
 568        message to be logged.  If the format string contains
 569        any % escapes requiring parameters, they should be
 570        specified as subsequent arguments (it's just like
 571        printf!).
 572
 573        The client ip and current date/time are prefixed to
 574        every message.
 575
 576        """
 577
 578        sys.stderr.write("%s - - [%s] %s\n" %
 579                         (self.address_string(),
 580                          self.log_date_time_string(),
 581                          format%args))
 582
 583    def version_string(self):
 584        """Return the server software version string."""
 585        return self.server_version + ' ' + self.sys_version
 586
 587    def date_time_string(self, timestamp=None):
 588        """Return the current date and time formatted for a message header."""
 589        if timestamp is None:
 590            timestamp = time.time()
 591        return email.utils.formatdate(timestamp, usegmt=True)
 592
 593    def log_date_time_string(self):
 594        """Return the current time formatted for logging."""
 595        now = time.time()
 596        year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
 597        s = "%02d/%3s/%04d %02d:%02d:%02d" % (
 598                day, self.monthname[month], year, hh, mm, ss)
 599        return s
 600
 601    weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
 602
 603    monthname = [None,
 604                 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
 605                 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
 606
 607    def address_string(self):
 608        """Return the client address."""
 609
 610        return self.client_address[0]
 611
 612    # Essentially static class variables
 613
 614    # The version of the HTTP protocol we support.
 615    # Set this to HTTP/1.1 to enable automatic keepalive
 616    protocol_version = "HTTP/1.0"
 617
 618    # MessageClass used to parse headers
 619    MessageClass = http.client.HTTPMessage
 620
 621    # hack to maintain backwards compatibility
 622    responses = {
 623        v: (v.phrase, v.description)
 624        for v in HTTPStatus.__members__.values()
 625    }
 626
 627
 628class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
 629
 630    """Simple HTTP request handler with GET and HEAD commands.
 631
 632    This serves files from the current directory and any of its
 633    subdirectories.  The MIME type for files is determined by
 634    calling the .guess_type() method.
 635
 636    The GET and HEAD requests are identical except that the HEAD
 637    request omits the actual contents of the file.
 638
 639    """
 640
 641    server_version = "SimpleHTTP/" + __version__
 642    extensions_map = _encodings_map_default = {
 643        '.gz': 'application/gzip',
 644        '.Z': 'application/octet-stream',
 645        '.bz2': 'application/x-bzip2',
 646        '.xz': 'application/x-xz',
 647    }
 648
 649    def __init__(self, *args, directory=None, **kwargs):
 650        if directory is None:
 651            directory = os.getcwd()
 652        self.directory = os.fspath(directory)
 653        super().__init__(*args, **kwargs)
 654
 655    def do_GET(self):
 656        """Serve a GET request."""
 657        f = self.send_head()
 658        if f:
 659            try:
 660                self.copyfile(f, self.wfile)
 661            finally:
 662                f.close()
 663
 664    def do_HEAD(self):
 665        """Serve a HEAD request."""
 666        f = self.send_head()
 667        if f:
 668            f.close()
 669
 670    def send_head(self):
 671        """Common code for GET and HEAD commands.
 672
 673        This sends the response code and MIME headers.
 674
 675        Return value is either a file object (which has to be copied
 676        to the outputfile by the caller unless the command was HEAD,
 677        and must be closed by the caller under all circumstances), or
 678        None, in which case the caller has nothing further to do.
 679
 680        """
 681        path = self.translate_path(self.path)
 682        f = None
 683        if os.path.isdir(path):
 684            parts = urllib.parse.urlsplit(self.path)
 685            if not parts.path.endswith('/'):
 686                # redirect browser - doing basically what apache does
 687                self.send_response(HTTPStatus.MOVED_PERMANENTLY)
 688                new_parts = (parts[0], parts[1], parts[2] + '/',
 689                             parts[3], parts[4])
 690                new_url = urllib.parse.urlunsplit(new_parts)
 691                self.send_header("Location", new_url)
 692                self.end_headers()
 693                return None
 694            for index in "index.html", "index.htm":
 695                index = os.path.join(path, index)
 696                if os.path.exists(index):
 697                    path = index
 698                    break
 699            else:
 700                return self.list_directory(path)
 701        ctype = self.guess_type(path)
 702        # check for trailing "/" which should return 404. See Issue17324
 703        # The test for this was added in test_httpserver.py
 704        # However, some OS platforms accept a trailingSlash as a filename
 705        # See discussion on python-dev and Issue34711 regarding
 706        # parseing and rejection of filenames with a trailing slash
 707        if path.endswith("/"):
 708            self.send_error(HTTPStatus.NOT_FOUND, "File not found")
 709            return None
 710        try:
 711            f = open(path, 'rb')
 712        except OSError:
 713            self.send_error(HTTPStatus.NOT_FOUND, "File not found")
 714            return None
 715
 716        try:
 717            fs = os.fstat(f.fileno())
 718            # Use browser cache if possible
 719            if ("If-Modified-Since" in self.headers
 720                    and "If-None-Match" not in self.headers):
 721                # compare If-Modified-Since and time of last file modification
 722                try:
 723                    ims = email.utils.parsedate_to_datetime(
 724                        self.headers["If-Modified-Since"])
 725                except (TypeError, IndexError, OverflowError, ValueError):
 726                    # ignore ill-formed values
 727                    pass
 728                else:
 729                    if ims.tzinfo is None:
 730                        # obsolete format with no timezone, cf.
 731                        # https://tools.ietf.org/html/rfc7231#section-7.1.1.1
 732                        ims = ims.replace(tzinfo=datetime.timezone.utc)
 733                    if ims.tzinfo is datetime.timezone.utc:
 734                        # compare to UTC datetime of last modification
 735                        last_modif = datetime.datetime.fromtimestamp(
 736                            fs.st_mtime, datetime.timezone.utc)
 737                        # remove microseconds, like in If-Modified-Since
 738                        last_modif = last_modif.replace(microsecond=0)
 739
 740                        if last_modif <= ims:
 741                            self.send_response(HTTPStatus.NOT_MODIFIED)
 742                            self.end_headers()
 743                            f.close()
 744                            return None
 745
 746            self.send_response(HTTPStatus.OK)
 747            self.send_header("Content-type", ctype)
 748            self.send_header("Content-Length", str(fs[6]))
 749            self.send_header("Last-Modified",
 750                self.date_time_string(fs.st_mtime))
 751            self.end_headers()
 752            return f
 753        except:
 754            f.close()
 755            raise
 756
 757    def list_directory(self, path):
 758        """Helper to produce a directory listing (absent index.html).
 759
 760        Return value is either a file object, or None (indicating an
 761        error).  In either case, the headers are sent, making the
 762        interface the same as for send_head().
 763
 764        """
 765        try:
 766            list = os.listdir(path)
 767        except OSError:
 768            self.send_error(
 769                HTTPStatus.NOT_FOUND,
 770                "No permission to list directory")
 771            return None
 772        list.sort(key=lambda a: a.lower())
 773        r = []
 774        try:
 775            displaypath = urllib.parse.unquote(self.path,
 776                                               errors='surrogatepass')
 777        except UnicodeDecodeError:
 778            displaypath = urllib.parse.unquote(path)
 779        displaypath = html.escape(displaypath, quote=False)
 780        enc = sys.getfilesystemencoding()
 781        title = 'Directory listing for %s' % displaypath
 782        r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
 783                 '"http://www.w3.org/TR/html4/strict.dtd">')
 784        r.append('<html>\n<head>')
 785        r.append('<meta http-equiv="Content-Type" '
 786                 'content="text/html; charset=%s">' % enc)
 787        r.append('<title>%s</title>\n</head>' % title)
 788        r.append('<body>\n<h1>%s</h1>' % title)
 789        r.append('<hr>\n<ul>')
 790        for name in list:
 791            fullname = os.path.join(path, name)
 792            displayname = linkname = name
 793            # Append / for directories or @ for symbolic links
 794            if os.path.isdir(fullname):
 795                displayname = name + "/"
 796                linkname = name + "/"
 797            if os.path.islink(fullname):
 798                displayname = name + "@"
 799                # Note: a link to a directory displays with @ and links with /
 800            r.append('<li><a href="%s">%s</a></li>'
 801                    % (urllib.parse.quote(linkname,
 802                                          errors='surrogatepass'),
 803                       html.escape(displayname, quote=False)))
 804        r.append('</ul>\n<hr>\n</body>\n</html>\n')
 805        encoded = '\n'.join(r).encode(enc, 'surrogateescape')
 806        f = io.BytesIO()
 807        f.write(encoded)
 808        f.seek(0)
 809        self.send_response(HTTPStatus.OK)
 810        self.send_header("Content-type", "text/html; charset=%s" % enc)
 811        self.send_header("Content-Length", str(len(encoded)))
 812        self.end_headers()
 813        return f
 814
 815    def translate_path(self, path):
 816        """Translate a /-separated PATH to the local filename syntax.
 817
 818        Components that mean special things to the local file system
 819        (e.g. drive or directory names) are ignored.  (XXX They should
 820        probably be diagnosed.)
 821
 822        """
 823        # abandon query parameters
 824        path = path.split('?',1)[0]
 825        path = path.split('#',1)[0]
 826        # Don't forget explicit trailing slash when normalizing. Issue17324
 827        trailing_slash = path.rstrip().endswith('/')
 828        try:
 829            path = urllib.parse.unquote(path, errors='surrogatepass')
 830        except UnicodeDecodeError:
 831            path = urllib.parse.unquote(path)
 832        path = posixpath.normpath(path)
 833        words = path.split('/')
 834        words = filter(None, words)
 835        path = self.directory
 836        for word in words:
 837            if os.path.dirname(word) or word in (os.curdir, os.pardir):
 838                # Ignore components that are not a simple file/directory name
 839                continue
 840            path = os.path.join(path, word)
 841        if trailing_slash:
 842            path += '/'
 843        return path
 844
 845    def copyfile(self, source, outputfile):
 846        """Copy all data between two file objects.
 847
 848        The SOURCE argument is a file object open for reading
 849        (or anything with a read() method) and the DESTINATION
 850        argument is a file object open for writing (or
 851        anything with a write() method).
 852
 853        The only reason for overriding this would be to change
 854        the block size or perhaps to replace newlines by CRLF
 855        -- note however that this the default server uses this
 856        to copy binary data as well.
 857
 858        """
 859        shutil.copyfileobj(source, outputfile)
 860
 861    def guess_type(self, path):
 862        """Guess the type of a file.
 863
 864        Argument is a PATH (a filename).
 865
 866        Return value is a string of the form type/subtype,
 867        usable for a MIME Content-type header.
 868
 869        The default implementation looks the file's extension
 870        up in the table self.extensions_map, using application/octet-stream
 871        as a default; however it would be permissible (if
 872        slow) to look inside the data to make a better guess.
 873
 874        """
 875        base, ext = posixpath.splitext(path)
 876        if ext in self.extensions_map:
 877            return self.extensions_map[ext]
 878        ext = ext.lower()
 879        if ext in self.extensions_map:
 880            return self.extensions_map[ext]
 881        guess, _ = mimetypes.guess_type(path)
 882        if guess:
 883            return guess
 884        return 'application/octet-stream'
 885
 886
 887# Utilities for CGIHTTPRequestHandler
 888
 889def _url_collapse_path(path):
 890    """
 891    Given a URL path, remove extra '/'s and '.' path elements and collapse
 892    any '..' references and returns a collapsed path.
 893
 894    Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
 895    The utility of this function is limited to is_cgi method and helps
 896    preventing some security attacks.
 897
 898    Returns: The reconstituted URL, which will always start with a '/'.
 899
 900    Raises: IndexError if too many '..' occur within the path.
 901
 902    """
 903    # Query component should not be involved.
 904    path, _, query = path.partition('?')
 905    path = urllib.parse.unquote(path)
 906
 907    # Similar to os.path.split(os.path.normpath(path)) but specific to URL
 908    # path semantics rather than local operating system semantics.
 909    path_parts = path.split('/')
 910    head_parts = []
 911    for part in path_parts[:-1]:
 912        if part == '..':
 913            head_parts.pop() # IndexError if more '..' than prior parts
 914        elif part and part != '.':
 915            head_parts.append( part )
 916    if path_parts:
 917        tail_part = path_parts.pop()
 918        if tail_part:
 919            if tail_part == '..':
 920                head_parts.pop()
 921                tail_part = ''
 922            elif tail_part == '.':
 923                tail_part = ''
 924    else:
 925        tail_part = ''
 926
 927    if query:
 928        tail_part = '?'.join((tail_part, query))
 929
 930    splitpath = ('/' + '/'.join(head_parts), tail_part)
 931    collapsed_path = "/".join(splitpath)
 932
 933    return collapsed_path
 934
 935
 936
 937nobody = None
 938
 939def nobody_uid():
 940    """Internal routine to get nobody's uid"""
 941    global nobody
 942    if nobody:
 943        return nobody
 944    try:
 945        import pwd
 946    except ImportError:
 947        return -1
 948    try:
 949        nobody = pwd.getpwnam('nobody')[2]
 950    except KeyError:
 951        nobody = 1 + max(x[2] for x in pwd.getpwall())
 952    return nobody
 953
 954
 955def executable(path):
 956    """Test for executable file."""
 957    return os.access(path, os.X_OK)
 958
 959
 960class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
 961
 962    """Complete HTTP server with GET, HEAD and POST commands.
 963
 964    GET and HEAD also support running CGI scripts.
 965
 966    The POST command is *only* implemented for CGI scripts.
 967
 968    """
 969
 970    # Determine platform specifics
 971    have_fork = hasattr(os, 'fork')
 972
 973    # Make rfile unbuffered -- we need to read one line and then pass
 974    # the rest to a subprocess, so we can't use buffered input.
 975    rbufsize = 0
 976
 977    def do_POST(self):
 978        """Serve a POST request.
 979
 980        This is only implemented for CGI scripts.
 981
 982        """
 983
 984        if self.is_cgi():
 985            self.run_cgi()
 986        else:
 987            self.send_error(
 988                HTTPStatus.NOT_IMPLEMENTED,
 989                "Can only POST to CGI scripts")
 990
 991    def send_head(self):
 992        """Version of send_head that support CGI scripts"""
 993        if self.is_cgi():
 994            return self.run_cgi()
 995        else:
 996            return SimpleHTTPRequestHandler.send_head(self)
 997
 998    def is_cgi(self):
 999        """Test whether self.path corresponds to a CGI script.
1000
1001        Returns True and updates the cgi_info attribute to the tuple
1002        (dir, rest) if self.path requires running a CGI script.
1003        Returns False otherwise.
1004
1005        If any exception is raised, the caller should assume that
1006        self.path was rejected as invalid and act accordingly.
1007
1008        The default implementation tests whether the normalized url
1009        path begins with one of the strings in self.cgi_directories
1010        (and the next character is a '/' or the end of the string).
1011
1012        """
1013        collapsed_path = _url_collapse_path(self.path)
1014        dir_sep = collapsed_path.find('/', 1)
1015        while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories:
1016            dir_sep = collapsed_path.find('/', dir_sep+1)
1017        if dir_sep > 0:
1018            head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
1019            self.cgi_info = head, tail
1020            return True
1021        return False
1022
1023
1024    cgi_directories = ['/cgi-bin', '/htbin']
1025
1026    def is_executable(self, path):
1027        """Test whether argument path is an executable file."""
1028        return executable(path)
1029
1030    def is_python(self, path):
1031        """Test whether argument path is a Python script."""
1032        head, tail = os.path.splitext(path)
1033        return tail.lower() in (".py", ".pyw")
1034
1035    def run_cgi(self):
1036        """Execute a CGI script."""
1037        dir, rest = self.cgi_info
1038        path = dir + '/' + rest
1039        i = path.find('/', len(dir)+1)
1040        while i >= 0:
1041            nextdir = path[:i]
1042            nextrest = path[i+1:]
1043
1044            scriptdir = self.translate_path(nextdir)
1045            if os.path.isdir(scriptdir):
1046                dir, rest = nextdir, nextrest
1047                i = path.find('/', len(dir)+1)
1048            else:
1049                break
1050
1051        # find an explicit query string, if present.
1052        rest, _, query = rest.partition('?')
1053
1054        # dissect the part after the directory name into a script name &
1055        # a possible additional path, to be stored in PATH_INFO.
1056        i = rest.find('/')
1057        if i >= 0:
1058            script, rest = rest[:i], rest[i:]
1059        else:
1060            script, rest = rest, ''
1061
1062        scriptname = dir + '/' + script
1063        scriptfile = self.translate_path(scriptname)
1064        if not os.path.exists(scriptfile):
1065            self.send_error(
1066                HTTPStatus.NOT_FOUND,
1067                "No such CGI script (%r)" % scriptname)
1068            return
1069        if not os.path.isfile(scriptfile):
1070            self.send_error(
1071                HTTPStatus.FORBIDDEN,
1072                "CGI script is not a plain file (%r)" % scriptname)
1073            return
1074        ispy = self.is_python(scriptname)
1075        if self.have_fork or not ispy:
1076            if not self.is_executable(scriptfile):
1077                self.send_error(
1078                    HTTPStatus.FORBIDDEN,
1079                    "CGI script is not executable (%r)" % scriptname)
1080                return
1081
1082        # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
1083        # XXX Much of the following could be prepared ahead of time!
1084        env = copy.deepcopy(os.environ)
1085        env['SERVER_SOFTWARE'] = self.version_string()
1086        env['SERVER_NAME'] = self.server.server_name
1087        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
1088        env['SERVER_PROTOCOL'] = self.protocol_version
1089        env['SERVER_PORT'] = str(self.server.server_port)
1090        env['REQUEST_METHOD'] = self.command
1091        uqrest = urllib.parse.unquote(rest)
1092        env['PATH_INFO'] = uqrest
1093        env['PATH_TRANSLATED'] = self.translate_path(uqrest)
1094        env['SCRIPT_NAME'] = scriptname
1095        if query:
1096            env['QUERY_STRING'] = query
1097        env['REMOTE_ADDR'] = self.client_address[0]
1098        authorization = self.headers.get("authorization")
1099        if authorization:
1100            authorization = authorization.split()
1101            if len(authorization) == 2:
1102                import base64, binascii
1103                env['AUTH_TYPE'] = authorization[0]
1104                if authorization[0].lower() == "basic":
1105                    try:
1106                        authorization = authorization[1].encode('ascii')
1107                        authorization = base64.decodebytes(authorization).\
1108                                        decode('ascii')
1109                    except (binascii.Error, UnicodeError):
1110                        pass
1111                    else:
1112                        authorization = authorization.split(':')
1113                        if len(authorization) == 2:
1114                            env['REMOTE_USER'] = authorization[0]
1115        # XXX REMOTE_IDENT
1116        if self.headers.get('content-type') is None:
1117            env['CONTENT_TYPE'] = self.headers.get_content_type()
1118        else:
1119            env['CONTENT_TYPE'] = self.headers['content-type']
1120        length = self.headers.get('content-length')
1121        if length:
1122            env['CONTENT_LENGTH'] = length
1123        referer = self.headers.get('referer')
1124        if referer:
1125            env['HTTP_REFERER'] = referer
1126        accept = self.headers.get_all('accept', ())
1127        env['HTTP_ACCEPT'] = ','.join(accept)
1128        ua = self.headers.get('user-agent')
1129        if ua:
1130            env['HTTP_USER_AGENT'] = ua
1131        co = filter(None, self.headers.get_all('cookie', []))
1132        cookie_str = ', '.join(co)
1133        if cookie_str:
1134            env['HTTP_COOKIE'] = cookie_str
1135        # XXX Other HTTP_* headers
1136        # Since we're setting the env in the parent, provide empty
1137        # values to override previously set values
1138        for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
1139                  'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
1140            env.setdefault(k, "")
1141
1142        self.send_response(HTTPStatus.OK, "Script output follows")
1143        self.flush_headers()
1144
1145        decoded_query = query.replace('+', ' ')
1146
1147        if self.have_fork:
1148            # Unix -- fork as we should
1149            args = [script]
1150            if '=' not in decoded_query:
1151                args.append(decoded_query)
1152            nobody = nobody_uid()
1153            self.wfile.flush() # Always flush before forking
1154            pid = os.fork()
1155            if pid != 0:
1156                # Parent
1157                pid, sts = os.waitpid(pid, 0)
1158                # throw away additional data [see bug #427345]
1159                while select.select([self.rfile], [], [], 0)[0]:
1160                    if not self.rfile.read(1):
1161                        break
1162                exitcode = os.waitstatus_to_exitcode(sts)
1163                if exitcode:
1164                    self.log_error(f"CGI script exit code {exitcode}")
1165                return
1166            # Child
1167            try:
1168                try:
1169                    os.setuid(nobody)
1170                except OSError:
1171                    pass
1172                os.dup2(self.rfile.fileno(), 0)
1173                os.dup2(self.wfile.fileno(), 1)
1174                os.execve(scriptfile, args, env)
1175            except:
1176                self.server.handle_error(self.request, self.client_address)
1177                os._exit(127)
1178
1179        else:
1180            # Non-Unix -- use subprocess
1181            import subprocess
1182            cmdline = [scriptfile]
1183            if self.is_python(scriptfile):
1184                interp = sys.executable
1185                if interp.lower().endswith("w.exe"):
1186                    # On Windows, use python.exe, not pythonw.exe
1187                    interp = interp[:-5] + interp[-4:]
1188                cmdline = [interp, '-u'] + cmdline
1189            if '=' not in query:
1190                cmdline.append(query)
1191            self.log_message("command: %s", subprocess.list2cmdline(cmdline))
1192            try:
1193                nbytes = int(length)
1194            except (TypeError, ValueError):
1195                nbytes = 0
1196            p = subprocess.Popen(cmdline,
1197                                 stdin=subprocess.PIPE,
1198                                 stdout=subprocess.PIPE,
1199                                 stderr=subprocess.PIPE,
1200                                 env = env
1201                                 )
1202            if self.command.lower() == "post" and nbytes > 0:
1203                data = self.rfile.read(nbytes)
1204            else:
1205                data = None
1206            # throw away additional data [see bug #427345]
1207            while select.select([self.rfile._sock], [], [], 0)[0]:
1208                if not self.rfile._sock.recv(1):
1209                    break
1210            stdout, stderr = p.communicate(data)
1211            self.wfile.write(stdout)
1212            if stderr:
1213                self.log_error('%s', stderr)
1214            p.stderr.close()
1215            p.stdout.close()
1216            status = p.returncode
1217            if status:
1218                self.log_error("CGI script exit status %#x", status)
1219            else:
1220                self.log_message("CGI script exited OK")
1221
1222
1223def _get_best_family(*address):
1224    infos = socket.getaddrinfo(
1225        *address,
1226        type=socket.SOCK_STREAM,
1227        flags=socket.AI_PASSIVE,
1228    )
1229    family, type, proto, canonname, sockaddr = next(iter(infos))
1230    return family, sockaddr
1231
1232
1233def test(HandlerClass=BaseHTTPRequestHandler,
1234         ServerClass=ThreadingHTTPServer,
1235         protocol="HTTP/1.0", port=8000, bind=None):
1236    """Test the HTTP request handler class.
1237
1238    This runs an HTTP server on port 8000 (or the port argument).
1239
1240    """
1241    ServerClass.address_family, addr = _get_best_family(bind, port)
1242
1243    HandlerClass.protocol_version = protocol
1244    with ServerClass(addr, HandlerClass) as httpd:
1245        host, port = httpd.socket.getsockname()[:2]
1246        url_host = f'[{host}]' if ':' in host else host
1247        print(
1248            f"Serving HTTP on {host} port {port} "
1249            f"(http://{url_host}:{port}/) ..."
1250        )
1251        try:
1252            httpd.serve_forever()
1253        except KeyboardInterrupt:
1254            print("\nKeyboard interrupt received, exiting.")
1255            sys.exit(0)
1256
1257if __name__ == '__main__':
1258    import argparse
1259
1260    parser = argparse.ArgumentParser()
1261    parser.add_argument('--cgi', action='store_true',
1262                       help='Run as CGI Server')
1263    parser.add_argument('--bind', '-b', metavar='ADDRESS',
1264                        help='Specify alternate bind address '
1265                             '[default: all interfaces]')
1266    parser.add_argument('--directory', '-d', default=os.getcwd(),
1267                        help='Specify alternative directory '
1268                        '[default:current directory]')
1269    parser.add_argument('port', action='store',
1270                        default=8000, type=int,
1271                        nargs='?',
1272                        help='Specify alternate port [default: 8000]')
1273    args = parser.parse_args()
1274    if args.cgi:
1275        handler_class = CGIHTTPRequestHandler
1276    else:
1277        handler_class = partial(SimpleHTTPRequestHandler,
1278                                directory=args.directory)
1279
1280    # ensure dual-stack is not disabled; ref #38907
1281    class DualStackServer(ThreadingHTTPServer):
1282        def server_bind(self):
1283            # suppress exception when protocol is IPv4
1284            with contextlib.suppress(Exception):
1285                self.socket.setsockopt(
1286                    socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
1287            return super().server_bind()
1288
1289    test(
1290        HandlerClass=handler_class,
1291        ServerClass=DualStackServer,
1292        port=args.port,
1293        bind=args.bind,
1294    )