Custom Web Server Responses with Python
Introduction
In my job sometimes I have to setup quick web servers to respond to request in certain ways. Python does a great job at making this easy without having to have any thing other than Python installed. I figured I would make a quick post to share some of the scripts I have created and why I created them. All the scripts are written in Python3.
Basic
If all I need is a web server that will serve files Python gives me a way to do it very easily and very quickly. Navigating to the directory that contains the files I want to server I can run python3 -m http.server
to start a web server that will serve the files in the directory its started from. If I need to specify the port it runs on its as simple as adding the port number to the end of the command.
Stepping it up
Sometimes I don't want to serve any files and want a web server that responds in specific ways to requests.
Hello, World!
When I just need a web server that responds with static content regardless of the request I have to write a script to do it. Below is the full script but I will break it down after.
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
message = "Hello!"
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Length", len(message))
self.end_headers()
self.wfile.write(bytes(message, "utf8"))
return
def run():
server = ('', 80)
httpd = HTTPServer(server, RequestHandler)
httpd.serve_forever()
run()
First, I need to import the a couple classes from the http.server
library.
from http.server import BaseHTTPRequestHandler, HTTPServer
After that I need to create my own class to handle requests that the server sees.
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
message = "Hello!"
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Length", len(message))
self.end_headers()
self.wfile.write(bytes(message, "utf8"))
return
The class above will handle GET requests and send a 200 response. If you notice I am specifying the protocol version and adding a message to the response.
After making the class I need to add a function to initialize the class and run the web server.
def run():
server = ('', 80)
httpd = HTTPServer(server, RequestHandler)
httpd.serve_forever()
run()
The function run
simply tells the interface and port to bind to. Using ''
instead of an IP simply binds to all interfaces. After that it initializes the HTTPServer
class and sends it my custom request handler class. Finally, it sets the server to function persistently.
Secure Hello
In some situations I need to have the server handle SSL traffic. Modifying the script with only a few lines makes this a breeze. Again, the full script is below followed by a breakdown of the new parts.
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import ssl
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
message = "Hello!"
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Length", len(message))
self.end_headers()
self.wfile.write(bytes(message, "utf8"))
return
def run():
server = ('', 443)
httpd = HTTPServer(server, RequestHandler)
httpd.socket = ssl.wrap_socket(httpd.socket,
keyfile="/path/to/private_key.pem",
certfile="/path/to/full_cert_chain.pem",
server_side=True)
httpd.serve_forever()
run()
The first thing is to import the ssl
python library by adding import ssl
to the script. The last thing is to modify the run
function to allow the server to handle SSL request.
def run():
server = ('', 443)
httpd = HTTPServer(server, RequestHandler)
httpd.socket = ssl.wrap_socket(httpd.socket,
keyfile="/path/to/private_key.pem",
certfile="/path/to/full_cert_chain.pem",
server_side=True)
httpd.serve_forever()
run()
First, I update the port to 443 but if you are using a custom port this doesn't matter. Second, I modify the socket thats created to use the ssl.warp_socket()
function. I pass that function the private key and certificate chain for a valid SSL certificate.
Collecting Information
A pretty common use in pentesting for having a simple web server up is to collect session cookies or tokens. The python3 -m http.server
display the request as they come in, but wouldn't it be better if it logged only the relevant information we wanted? This is where some of the power of making an actual Python script come in.
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.end_headers()
if '?q=' in self.path:
q = parse_qs(self.path[2:])["q"]
with open('log.txt','a') as f:
for cookie in q:
f.write(str(cookie)+'\n')
return
def run():
server = ('', 8000)
httpd = HTTPServer(server, RequestHandler)
httpd.serve_forever()
run()
There are some new things in the above script. First I am importing a function called parse_qs
from urllib.parse
. What this function does is it allows me to parse GET parameters in the request fairly easily.
Next, there is a big chunk of logic added to the script.
if '?q=' in self.path:
q = parse_qs(self.path[2:])["q"]
with open('log.txt','a') as f:
for cookie in q:
f.write(str(cookie)+'\n')
What the above is doing is checking if there is a parameter called q
at the beginning of the request. Then it actually parses the parameter. Finally, appends the value of q
in a text file.
Summary
I hadn't posted anything in a while so I decided to share these scripts in a quick post. Hopefully, down the road they save some people time like they have me.