A simple reverse proxy in Python

6
minutes
Mis à jour le
15/9/2019

Share this post

In this article, I’ll talk you though how I created a reverse proxy in python able to inject security headers to solve one of our data issue that we had on one project. I work in a corporate…

#
DevOps
#
Python

In this article, I’ll talk you though how I created a reverse proxy in python able to inject security headers to solve one of our data issue that we had on one project.

I work in a corporate environment that strongly promote separation of the different development environnements.
This is without a doubt a good practice in theory. However, what ended up happening is that our pre-production environment had a lot of incoherent tests setup across our 20 or so external services.Next to that, in our staging environment we were struggling to find even one test set that would allows us to run any requests completely.

Seeing that, I decided to:

  • Dump my preproduction data and load it in staging
  • Create a bridge between environment capable of authenticating itself.
  • Send all the external call devoid of side effect through this bridge

You can check out the final result on GitHub or follow the rest of this article to learn more about the building process.

Why Python ?

There are many reasons why I went for python as my choice language for the implementation. Excluding the fact that it’s syntax is arguably one of the easiest to understand, here are some of the reasons:

  • It’s present on most “modern” servers
  • It comes with a package manager (PIP)
  • Integrate well with virtual environment that can be installed with user permission
  • You don’t need root access to the server

But wait! What is a reverse proxy ?

If you’re not familiar with the concept you could read the Wikipedia page on the subject. Basically, a reverse proxy is a server that sit between you and the real destination of your request. It will query the real ressource you want to access for you and give you back the response it got after having tampered with it.

In a way it acts as a man in the middle if you are familiar with the concept.

Here is a schema of what we’re going to build.

schéma

The reverse proxy

The requirements

The reverse proxy we’re trying to build will have the following characteristics:

  • Listen to incoming requests
  • Change the target host name to the real target
  • Inject headers
  • Send the requests
  • Transmit the response as is

Understanding HTTP requests

Even if not necessary, a good understanding of the HTTP protocol can help you understand the inner workings of the tools we’re using.

In it’s simplest form an HTTP request will look like that.

POST /money HTTP/1.1 Host: bank.com User-Agent: HTTPTool/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 55 amount=100000&account=3000300000012736492817237400676

What we aim to do is the change the HOST header and add a new line with the security header (e.g Authorization: Bearer token)

But the HTTP protocol allows for much more complexity with chunked and or zipped requests and response.

Let’s make thing easier with Web Servers.

Let’s start by building a Web Server that will listen to incoming requests. Luckily Python comes with a Web Server implementation that’s quite straightforward to user.

from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer class ProxyHTTPRequestHandler(BaseHTTPRequestHandler):    protocol_version = 'HTTP/1.0'    def do_GET(self, body=True):        try:            req_header = self.parse_headers()            self.send_response(200)            self.send_resp_headers(req_header, 11)            self.wfile.write('Hello world')            return        finally:            self.finish() if __name__ == '__main__':    server_address = ('127.0.0.1', 8081)    httpd = HTTPServer(server_address, ProxyHTTPRequestHandler)    print('http server is running')    httpd.serve_forever()

With this we have fulfilled the first of our requirements, that is, we have a reverse proxy that is listening to incoming requests.

Injecting Headers

In the previous I did not show the function in charge of parsing the request header. This is where the magic of header injection will happen. Indeed the requests header are going to be used and sent to the real target so this is the perfect place to inject any complementary headers.

def do_GET(self, body=True):        try:            req_header = self.parse_headers()            [...]    def parse_headers(self):        req_header = {}        for line in self.headers.headers:            line_parts = [o.strip() for o in line.split(':', 1)]            if len(line_parts) == 2:                req_header[line_parts[0]] = line_parts[1]        return inject_auth(req_header)    def inject_auth(self, headers):        headers['Authorizaion'] = 'Bearer secret'        return headers

Once the headers are parse using the BaseHTTPRequestHandler syntax, injecting headers becomes as easy as adding a new value in a dictionary.

Sending the request to the real target

All that’s left now that we have a request with some additional headers is to send it to our desired target. For that I’m going to use a really powerful library that was made to handle HTTP requests called requests.

You can install it by running pipenv install requests in your favorite terminal.

Once that’s done, the following bit of code is all that’s needed to send the modified request.

def do_GET(self, body=True):        try:            # Parse request            hostname = 'preprod.bank.com'            url = 'https://{}{}'.format(hostname, self.path)            req_header = self.parse_headers()            # Call the target service            resp = requests.get(url, headers=req_header, verify=False)            # Respond with the requested data            self.send_response(resp.status_code)            self.send_resp_headers(resp.headers)            self.wfile.write(resp.content)

Again you can check the final version on my GitHub

Now what ?

Well I guess that you can let your imagination bring you where you need to be but if you need some hint here are some of the things you can use such a custom reverse proxy for:

  • Adding CORS to connect to a backend that doesn’t provide them directly
  • Injecting secret hidden from your front