Hackerman's Hacking Tutorials

The knowledge of anything, since all things have causes, is not acquired or complete unless it is known by its causes. - Avicenna

Nov 15, 2020 - 3 minute read - Comments - python

Customizing Python's SimpleHTTPServer

The other day I customized the Python built-in SimpleHTTPServer with some routes. I did not find a lot of info about it (most use it to serve files). This is how I did some basic customization.

This is for Python 3.8.6 (which what I have in my testing VM) but it should work on Python 3.9 (and probably the same for Python 2).

Code is at https://github.com/parsiya/Parsia-Code/tree/master/python-simplehttpserver.

How to Serve Files

python -m http.server 8080 --bind 127.0.0.1.

Custom GET Responses

But I needed to customize the path. Let's start with a simple implementation. We need to create our own BaseHTTPRequestHandler.

from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        pass

httpd = HTTPServer(('localhost', 10000), MyHandler)
httpd.serve_forever()

To respond to GET requests we need to add code to do_GET. Let's say we want to return a 200 response that says It works!.

# 01.py
def do_GET(self):
    # send 200 response
    self.send_response(200)
    # send response headers
    self.end_headers()
    # send the body of the response
    self.wfile.write(bytes("01.py", "utf-8"))
01.py 01.py

Custom Response Headers

Note the server adds some default headers. To modify these we can use send_header before calling end_headers. This is very useful for adding the Content-Type header.

# 02.py
def do_GET(self):
    # send 200 response
    self.send_response(200)
    # add our own custom header
    self.send_header("myheader", "myvalue")
    # send response headers
    self.end_headers()
    # send the body of the response
    self.wfile.write(bytes("It Works!", "utf-8"))
02.py 02.py

To override a header we cannot use send_header because it will just add it as a new header to the response. Based on the documentation it seems like the Date and Server response headers cannot be changed :(.

Read Request Headers

I needed to read the incoming request headers. These are stored in the headers object. It is of type http.client.HTTPMessage which is a subclass of email.message.Message.

We can get the value of any header by name with headers.get("header name"). To get all values for a specific header (because headers can be repeated) use headers.get_all("header name").

# 03.py
def do_GET(self):
    # get the value of the "Authorization" header and echo it.
    authz = self.headers.get("authorization")
    # send 200 response
    self.send_response(200)
    # send response headers
    self.end_headers()
    # send the body of the response
    self.wfile.write(bytes(authz, "utf-8"))

Note: Header names are not case-sensitive in HTTP (or here).

03.py 03.py

Reading The Body of POST Requests

To handle POST requests we need to implement do_POST (d'oh). To read the body of the POST request we:

  1. Read the Content-Length header in the incoming request.
  2. Read that many bytes from self.rfile.

    1. I could not find a way to read "all bytes" in rfile. I had to rely on the header.

      def do_POST(self):
      # read the content-length header
      content_length = int(self.headers.get("Content-Length"))
      # read that many bytes from the body of the request
      body = self.rfile.read(content_length)
      
      self.send_response(200)
      self.end_headers()
      # echo the body in the response
      self.wfile.write(body)
04.py 04.py

Server Over TLS

First, you need to create a private key and certificate in pem format. To create a self-signed certificate/key in one go:

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem

Then modify the last lines of the script to:

httpd = HTTPServer(('localhost', 443), MyHandler)
httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True, certfile="certificate.pem", keyfile="key.pem")
httpd.serve_forever()

The Same-Origin Policy Gone Wild

comments powered by Disqus