SocketServer rfile.read() very very slow

3.9k views Asked by At

I am building an HTTPS proxy which can forward all SSL traffic. It is called a transparent tunneling. Anyway, I have a problem with Python's socketserver. When I called rfile.read(), it takes a very long time to return. Even I used select to make sure the I/O is ready, it still takes a very long time. Usually 30s and the client's socket is closed because of timeout. I cannot make the socket unblocking, because I need to read the data first, and then forward all the data I just read to remote server, it must be returned with data first.

My code is following:

import SocketServer
import BaseHTTPServer
import socket
import threading
import httplib
import time
import os
import urllib
import ssl
import copy

from history import *
from http import *
from https import *
from logger import Logger

DEFAULT_CERT_FILE = "./cert/ncerts/proxpy.pem"

proxystate = None

class ProxyHandler(SocketServer.StreamRequestHandler):
    def __init__(self, request, client_address, server):
        self.peer = False
        self.keepalive = False
        self.target = None
        self.tunnel_mode = False
        self.forwardSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Just for debugging
        self.counter = 0
        self._host = None
        self._port = 0

        SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)

    def createConnection(self, host, port):
        global proxystate

        if self.target and self._host == host:
            return self.target

        try:
            # If a SSL tunnel was established, create a HTTPS connection to the server
            if self.peer:
                conn = httplib.HTTPSConnection(host, port)
            else:
                # HTTP Connection
                conn = httplib.HTTPConnection(host, port)
        except HTTPException as e:
            proxystate.log.debug(e.__str__())

        # If we need a persistent connection, add the socket to the dictionary
        if self.keepalive:
            self.target = conn

        self._host = host
        self._port = port

        return conn

    def sendResponse(self, res):
        self.wfile.write(res)

    def finish(self):
        if not self.keepalive:
            if self.target:
                self.target.close()
            return SocketServer.StreamRequestHandler.finish(self)

        # Otherwise keep-alive is True, then go on and listen on the socket
        return self.handle()

    def handle(self):
        global proxystate

        if self.keepalive:
            if self.peer:
                HTTPSUtil.wait_read(self.request)
            else:
                HTTPUtil.wait_read(self.request)

            # Just debugging
            if self.counter > 0:
                proxystate.log.debug(str(self.client_address) + ' socket reused: ' + str(self.counter))
            self.counter += 1

        if self.tunnel_mode:
            HTTPUtil.wait_read(self.request)
            print "++++++++++++++++++++++++++"
            req=self.rfile.read(4096) #This is the line take very lone time!
            print "----------------------------------"
            data = self.doFORWARD(req)
            print "**************************************"
            if len(data)!=0:
                self.sendResponse(data)
                return

        try:
            req = HTTPRequest.build(self.rfile)
        except Exception as e:
            proxystate.log.debug(e.__str__() + ": Error on reading request message")
            return

        if req is None:
            return

        # Delegate request to plugin
        req = ProxyPlugin.delegate(ProxyPlugin.EVENT_MANGLE_REQUEST, req.clone())

        # if you need a persistent connection set the flag in order to save the status
        if req.isKeepAlive():
            self.keepalive = True
        else:
            self.keepalive = False

        # Target server host and port
        host, port = ProxyState.getTargetHost(req)

        if req.getMethod() == HTTPRequest.METHOD_GET:
            res = self.doGET(host, port, req)
            self.sendResponse(res)
        elif req.getMethod() == HTTPRequest.METHOD_POST:
            res = self.doPOST(host, port, req)
            self.sendResponse(res)
        elif req.getMethod() == HTTPRequest.METHOD_CONNECT:
            res = self.doCONNECT(host, port, req)

    def _request(self, conn, method, path, params, headers):
        global proxystate
        conn.putrequest(method, path, skip_host = True, skip_accept_encoding = True)
        for header,v in headers.iteritems():
            # auto-fix content-length
            if header.lower() == 'content-length':
                conn.putheader(header, str(len(params)))
            else:
                for i in v:
                    conn.putheader(header, i)
        conn.endheaders()

        if len(params) > 0:
            conn.send(params)

    def doRequest(self, conn, method, path, params, headers):
        global proxystate
        try:
            self._request(conn, method, path, params, headers)
            return True
        except IOError as e:
            proxystate.log.error("%s: %s:%d" % (e.__str__(), conn.host, conn.port))
            return False



    def doCONNECT(self, host, port, req):
        #global proxystate
        self.tunnel_mode = True
        self.tunnel_host = host
        self.tunnel_port = port
        #socket_req = self.request
        #certfilename = DEFAULT_CERT_FILE
        #socket_ssl = ssl.wrap_socket(socket_req, server_side = True, certfile = certfilename, 
                                     #ssl_version = ssl.PROTOCOL_SSLv23, do_handshake_on_connect = False)
        HTTPSRequest.sendAck(self.request)
        #print "Send ack to the peer %s on port %d for establishing SSL tunnel" % (host, port)
        print "into forward mode: %s : %s" % (host, port)
        '''
        host, port = socket_req.getpeername()
        proxystate.log.debug("Send ack to the peer %s on port %d for establishing SSL tunnel" % (host, port))

        while True:
            try:
                socket_ssl.do_handshake()
                break
            except (ssl.SSLError, IOError) as e:
                # proxystate.log.error(e.__str__())
                print e.__str__()
                return

        # Switch to new socket
        self.peer    = True
        self.request = socket_ssl
'''
        self.setup()
        #self.handle()

    def doFORWARD(self, data):
        host, port = self.request.getpeername()
        #print "client_host", host
        #print "client_port", port
        try:
            print "%s:%s===>data read, now sending..."%(host,port)
            self.forwardSocket.connect((self.tunnel_host,self.tunnel_port))
            self.forwardSocket.sendall(data)
            print data
            print "%s:%s===>sent %d bytes to server"%(host,port,len(data))
            select.select([self.forwardSocket], [], [])
            chunk = self.forwardSocket.recv(4096)
            print chunk
            print "%s:%s===>receive %d bytes from server"%(host,port,len(chunk))
            return chunk
        except socket.error as e:
            print e.__str__()
            return ''


class ThreadedHTTPProxyServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    allow_reuse_address = True

class ProxyServer():    
    def __init__(self, init_state):
        global proxystate
        proxystate = init_state
        self.proxyServer_port = proxystate.listenport
        self.proxyServer_host = proxystate.listenaddr

    def startProxyServer(self):
        global proxystate

        self.proxyServer = ThreadedHTTPProxyServer((self.proxyServer_host, self.proxyServer_port), ProxyHandler)

        # Start a thread with the server (that thread will then spawn a worker
        # thread for each request)
        server_thread = threading.Thread(target = self.proxyServer.serve_forever)

        # Exit the server thread when the main thread terminates
        server_thread.setDaemon(True)
        proxystate.log.info("Server %s listening on port %d" % (self.proxyServer_host, self.proxyServer_port))
        server_thread.start()

        while True:
            time.sleep(0.1)

This is driving me crazy, because when I forward normal HTTP traffic, the read() returns immediately with the data. But every time when the client sent SSL traffic (binary), the read() takes very long time to return! And I think there is some mechanics that the read() only returns when the request socket is closed by remote client. Does anyone know how to make the read() fast? I tried recv() and readline(), both of them is as slow as read() when handling binary traffic!

1

There are 1 answers

0
lxknvlk On

I had similar problem on my python server while trying to send image to it via http.

rfile.read took 1700ms for a 800kb image and 4500ms for a 4.5mb image. I was also doing this through a limited network speed on a vpn.

It appears that the rfile.read downloads/uploads the file. By improving connection/network speed between client-server i have reduced 1700ms read to less than 50ms.