#!/usr/bin/env python import sys import socket import thread import select import base64 import common import mimetools from SocketServer import TCPServer, ThreadingMixIn, BaseRequestHandler import logging logging.basicConfig(level=logging.DEBUG, filename='HTTor_OR.log', format='%(asctime)s %(levelname)s %(message)s') INACTIVITY_TIMEOUT = 360 DATA_BUF_SIZE = 1500 LOG_PAYLOAD = False class MiniHTTPServer(common.HTTPObfuscator): protocol_version = "HTTP/1.1" def __init__(self, sock): common.HTTPObfuscator.__init__(self, sock) self.key = None self.pending = "" def send_http(self, data): if self.key is None: self.pending = self.pending + data return to_send = self.obfuscate(self.pending + data) self.pending = "" self.send_response(200, "OK") self.send_header("Content-Type", "image/png") self.send_header("Content-Length", str(len(to_send))) self.end_headers() self.send(to_send) self.flush() def send_error(self, code, message): self.send_response(code, message) self.send_header("Content-Type", "text/html") self.send_header('Connection', 'close') self.end_headers() def send_response(self, code, message): self.send("%s %d %s\r\n" % (self.protocol_version, code, message)) def parse_request(self, requestline): command = None # set in case of error on the first line if requestline[-2:] == '\r\n': requestline = requestline[:-2] elif requestline[-1:] == '\n': requestline = requestline[:-1] words = requestline.split() if len(words) == 3: [command, path, version] = words if version[:5] != 'HTTP/': self.send_error(400, "Bad request version (%r)" % version) return False try: base_version_number = version.split('/', 1)[1] version_number = base_version_number.split(".") # RFC 2145 section 3.1 says there can be only one "." and # - major and minor numbers MUST be treated as # separate integers; # - HTTP/2.4 is a lower version than HTTP/2.13, which in # turn is lower than HTTP/12.3; # - Leading zeros MUST be ignored by recipients. if len(version_number) != 2: raise ValueError version_number = int(version_number[0]), int(version_number[1]) except (ValueError, IndexError): self.send_error(400, "Bad request version (%r)" % version) return False if version_number >= (2, 0): self.send_error(505, "Invalid HTTP Version (%s)" % base_version_number) return False elif not words: return False else: self.send_error(400, "Bad request syntax (%r)" % requestline) return False # Examine the headers and look for a Connection directive headers = mimetools.Message(self.rfile, 0) logging.debug("Parsed request as: %s %s %s (%s)"%( command, path, version, common.format_headers(headers))) return command, path, version, headers def read_http(self): requestline = self.rfile.readline() logging.debug("Received request '%s'"%requestline.strip()) if not requestline: return '' request = self.parse_request(requestline) if not request: return None else: command, path, version, headers = request if command == 'GET': to_send = "" self.key = base64.b32decode(headers["Cookie"]) self.send_response(200, "OK") self.send_header("Content-Type", "text/html") self.send_header("Content-Length", str(len(to_send))) self.end_headers() self.wfile.write(to_send) self.wfile.flush() logging.debug("Processed GET request") return None elif command == 'POST': data = self.rfile.read(int(headers.getheader('Content-Length'))) if LOG_PAYLOAD: logged_body = repr(data) else: logged_body = 'skipped' logging.debug("Processed POST request: %s"%logged_body) return self.deobfuscate(data) def forward(client_sock, server_sock): octets_in, octets_out = 0, 0 try: sockslist = [client_sock.sock, server_sock] while True: readables, writeables, exceptions = select.select( sockslist, [], [], INACTIVITY_TIMEOUT) if (exceptions): return "exception on fd" if (readables, writeables, exceptions) == ([], [], []): return "timeout" for readable_sock in readables: if readable_sock == client_sock.sock: write_socket = server_sock read_socket = client_sock else: write_socket = client_sock read_socket = server_sock if isinstance(read_socket, MiniHTTPServer): data = read_socket.read_http() else: data = read_socket.recv(DATA_BUF_SIZE) if data: if isinstance(write_socket, MiniHTTPServer): write_socket.send_http(data) else: write_socket.send(data) # This is only for future logging/stats. if readable_sock == client_sock: octets_out += len(data) else: octets_in += len(data) elif data == '': # The sock is readable but nothing can be read. # This means a poorly detected connection close. return "readable, but no data" except socket.error, e: return "exception: %s"%e finally: logging.info("%s: %s octets in and %s octets out"%(thread.get_ident(), octets_in, octets_out)) class ThreadingTCPServer(ThreadingMixIn, TCPServer): def __init__(self, server_address, RequestHandlerClass): self.allow_reuse_address = True TCPServer.__init__(self, server_address, RequestHandlerClass) class SimpleProxyHandler(BaseRequestHandler): def handle(self): logging.info("Received connection from %s"%(str(self.client_address))) tor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: addr = sys.argv[2].split(':') assert (len(addr) == 2) addr = (addr[0], int(addr[1])) logging.info("%s: Connecting to %s"%(thread.get_ident(), addr)) tor_socket.connect(addr) server = MiniHTTPServer(self.request) result = forward(server, tor_socket) logging.info("Connection closed (%s)"%result) finally: server.close() tor_socket.close() if __name__ == "__main__": listen_addr = sys.argv[1].split(':', 1) if len(listen_addr) == 2: listen_addr = (listen_addr[0], int(listen_addr[1])) else: listen_addr = ('', int(listen_addr[0])) logging.info("Listening on %s"%str(listen_addr)) server = ThreadingTCPServer(listen_addr, SimpleProxyHandler) server.serve_forever()