Aug. 29, 2012, midnight

A PYTHON PROXY IN LESS THAN 100 LINES OF CODE

Update: The code provided in this post is outdated. For an updated version go to github

What is a Proxy?

It's a intermediary server intended to act in name of a client, and sometimes to do something useful with the data before it reaches the original target. Let's see a picture:

alt text

My idea was to produce a proxy using only the default python library, and to guide me during development I set the following:

Each new client connection to our proxy must generate a new connection to the original target. Each data packet that reaches our proxy must be forwarded to the original target. Each data packet received from the target must be sent back to the correct client The proxy must accept multiple clients Must be fast Must have a low resource usage

In [ ]:
#!/usr/bin/python
# This is a simple port-forward / proxy, written using only the default python
# library. If you want to make a suggestion or fix something you can contact-me
# at voorloop_at_gmail.com
# Distributed over IDC(I Don't Care) license
import socket
import select
import time
import sys

# Changing the buffer_size and delay, you can improve the speed and bandwidth.
# But when buffer get to high or delay go too down, you can broke things
buffer_size = 4096
delay = 0.0001
forward_to = ('smtp.zaz.ufsk.br', 25)

class Forward:
    def __init__(self):
        self.forward = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def start(self, host, port):
        try:
            self.forward.connect((host, port))
            return self.forward
        except Exception, e:
            print e
            return False

class TheServer:
    input_list = []
    channel = {}

    def __init__(self, host, port):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server.bind((host, port))
        self.server.listen(200)

    def main_loop(self):
        self.input_list.append(self.server)
        while 1:
            time.sleep(delay)
            ss = select.select
            inputready, outputready, exceptready = ss(self.input_list, [], [])
            for self.s in inputready:
                if self.s == self.server:
                    self.on_accept()
                    break

                self.data = self.s.recv(buffer_size)
                if len(self.data) == 0:
                    self.on_close()
                    break
                else:
                    self.on_recv()

    def on_accept(self):
        forward = Forward().start(forward_to[0], forward_to[1])
        clientsock, clientaddr = self.server.accept()
        if forward:
            print clientaddr, "has connected"
            self.input_list.append(clientsock)
            self.input_list.append(forward)
            self.channel[clientsock] = forward
            self.channel[forward] = clientsock
        else:
            print "Can't establish connection with remote server.",
            print "Closing connection with client side", clientaddr
            clientsock.close()

    def on_close(self):
        print self.s.getpeername(), "has disconnected"
        #remove objects from input_list
        self.input_list.remove(self.s)
        self.input_list.remove(self.channel[self.s])
        out = self.channel[self.s]
        # close the connection with client
        self.channel[out].close()  # equivalent to do self.s.close()
        # close the connection with remote server
        self.channel[self.s].close()
        # delete both objects from channel dict
        del self.channel[out]
        del self.channel[self.s]

    def on_recv(self):
        data = self.data
        # here we can parse and/or modify the data before send forward
        print data
        self.channel[self.s].send(data)

if __name__ == '__main__':
        server = TheServer('', 9090)
        try:
            server.main_loop()
        except KeyboardInterrupt:
            print "Ctrl C - Stopping server"
            sys.exit(1)

Code explanation

class Forward()

The Forward class is the one responsible for establishing a connection between the proxy and the remote server(original target).

def main_loop(self)

The inputlist stores all the avaiable sockets that will be managed by select.select, the first one to be appended is the server socket itself, each new connection to this socket will trigger the onaccept() method.

If the current socket inputready (returned by select) is not a new connection, it will be considered as incoming data(maybe from server, maybe from client), if the data lenght is 0 it's a close request, otherwise the packet should be forwarded to the correct endpoint.

def on_accept(self)

This method creates a new connection with the original target (proxy -> remote server), and accepts the current client connection (client->proxy). Both sockets are stored in inputlist, to be then handled by mainloop. A "channel" dictionary is used to associate the endpoints(client<=>server).

def on_recv(self)

This method is used to process and forward the data to the original destination ( client <- proxy -> server ).

def on_close(self)

Disables and removes the socket connection between the proxy and the original server and the one between the client and the proxy itself.

That's it.

comments powered by Disqus