Personal IPv6/IPv4 Python Webserver

Personal IPv6/IPv4 Python Webserver


Why have a personal webserver? Easy, distribution of files. Sure you could use a thumb drive, but if you want to distribute a Pi image to everyone in the meeting, putting it on a personal webserver will make the transfer go much faster.

Why support IPv6 and IPv4? Not only is it cool, but it is very easy to do. IPv6 is the future of the internet, setting up a little personal webserver that supports both protocols will help you learn about the the future.

Why Python? Python is a wonderful programming language, and getting only better with version 3. There are libraries for most needs, including one which serves up the web. And it runs just about anywhere that Python runs (Windows, Linux, BSD, Mac, Pi, ODROID, etc)

Python module SimpleHTTPServer

The python module SimpleHTTPServer supports IPv4 out of the box with the simple command:

python -m SimpleHTTPServer

However it does not support IPv6. There is no one line equivalent to support IPv6, so a small script is required.

Looking at the code

The ipv6-httpd script is comprised of 4 sections:

  1. Initialization of the HTTPServer object (from SimpleHTTPServer library)
  2. Creation of a custom handler to support Virtual Paths
  3. Signal handling to permit cleaner quitting of the program
  4. Class creation (of HTTPServerV6) to support IPv6

As with all Python scripts, the details roll backwards from the bottom. Initialization occurs in main

def main():
    global server
    server = HTTPServerV6(('::', listen_port), MyHandler)
    print('Listening on port:' + str(listen_port) + '\nPress ^C to quit')
    server.serve_forever()
    os._exit(0)

A custom handler is created to support Virtual Paths. In this handler, if the URL includes the path /ip, the browser client’s IP address is returned in an short HTML page. Otherwise, if the path is a directory, an index of the directory will be returned, or if the path is a file, the file will be returned (e.g. downloaded)

class MyHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        # if path is /ip then print client IP address (v4 or v6)
        if self.path == '/ip':
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            answer = 'Your IP address is ' + self.client_address[0]
            # convert string 'answer' to bytes for buffer output
            self.wfile.write(str.encode(answer))
            return
        else:
            return SimpleHTTPRequestHandler.do_GET(self)

Signal handling makes exiting the program a little cleaner. Because server.serve_forever() does go on forever (an infinite loop), there must be a way to end the loop. There is a nice call sever.shutdown() however than hangs the server, it doesn’t exit, it doesn’t continue to serve, it is just stuck in dead lock.

It is designed to be used in a multi-threaded program, which is beyond the scope of the personal webserver. So in order to shut down cleanly (without a stack trace), we trap the ^C (or SIGINT) and quit from there.

# signal handlder for SIGINT
def sigint_handler(signal, frame):
    shutdown_requested = True    
    print("\nCaught SIGINT, dying")
    os._exit(0)

# register SIGINT signal handler
signal.signal(signal.SIGINT, sigint_handler)

Finally the IPv6 part

This is all good, but it still doesn’t serve over IPv6. In order to support IPv6, we use a bit of object oriented inheritance trickery to modify the default of an existing class HTTPServer

class HTTPServerV6(HTTPServer):
    address_family = socket.AF_INET6

This creates a new class (which is used in our server ) with the address_family set to AF_INET6 (aka IPv6).

Using IPv6 mapped addresses to support IPv4

So how does it work for IPv4 if the address_family has been set to IPv6? The designers foresaw this problem, and created mapped addresses which are IPv4 addresses embedded inside an IPv6 address. An example for 192.168.1.11 would become a mapped address of ::FFFF:192.168.1.11. When an IPv4 packet arrives on the listening port (8080), the IP stack converts the IPv4 address to a mapped address and forwards it to the application which is listening to IPv6-only.

Running the code

Now that we have a server which supports both IPv4 and IPv6, all we need to do is cd to the directory we wish to share, and start the server

$ cd public/
$ ~/bin/ipv6-httpd.py 
Listening on port:8080
Press ^C to quit
2001:db8:ebbd:0:4d18:71cd:b814:9508 - - [09/Jul/2017 11:49:41] "GET / HTTP/1.1" 200 -
^CCaught SIGINT, dying

The webserver log is sent to standard out (stdout), and can be redirected to a file if desired. In the above example, an IPv6 client ..:9508 requests an index, then the server is terminated with a ^C.

Want to server from a different directory? Stop the server, cd to another directory, and restart the server, it will now serve files from the new current working directory (cwd) location.

Security (or lack there of)

This is a personal webserver, designed to be started and stopped whenever you need it. It is NOT a production quality webserver that you should put on the internet.

It does not server encrypted pages (read: no SSL/TLS), and it is single threaded, which means if a second request comes in while serving the first request, the second request will have to wait.

Demo

Share your Documents direcitory.

$ cd Documents/
$ ~/bin/ipv6-httpd.py 
Listening on port:8080

Share your Webcam (internally).

$ cd Pictures/
# start webcam taking photos every 5 seconds
$ fswebcam -r 640x480 --jpeg 85 -S 10 -l 5 -b web-cam-shot.jpg
# start the personal webserver
$ ~/bin/ipv6-httpd.py 
Listening on port:8080


Point your browser to fd11::11/cam1.html. The page will reload every 5 seconds, and there will be a new picture every 5 seconds (from the WebCam).

Notes: