pythonsocketssocks

How to bind a http.server to a remote socks proxy?


I've a webserver that is going to answer requests at a remote socks5 proxy.

I want the webserver to connect to the proxy, send the bind command, and listen to a connection.

How can I set up a http.server to bind and listen on a remote port?

I have tried to override server_bind without success:

class TestHTTPServer ( http.server.HTTPServer ):
    def __init__ ( self, server_address, RequestHandlerClass, proxy_host=None, proxy_port=None, bind_and_activate=True ):
        if ':' in (str)(server_address[0]):
            self.address_family = socket.AF_INET6
        else:
            self.address_family = socket.AF_INET
        
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        
        super().__init__ ( server_address, RequestHandlerClass, bind_and_activate )
    
    def serve_forever ( self, poll_interval = 0.5 ):
        self.socket.settimeout ( poll_interval )
        while not stop_unittest_event.is_set ():
            try:
                self.handle_request ()
            except OSError:
                pass

    def server_bind ( self ):
        if self.proxy_host is None or self.proxy_port is None:
            super().server_bind ()
        else:
            self.socket = socks.socksocket()
            self.socket.set_proxy(socks.SOCKS5, self.server_address[0], self.server_address[1])
            self.socket.bind(self.server_address)

It seems as set_proxy only handles client requests (not servers). How can this code be changed to listen on a socks5 port?

It is something an FTP-server does in active mode.


Solution

  • class TestHTTPServerServer ( http.server.HTTPServer ):
        def __init__ ( self, server_address, RequestHandlerClass, bind_and_activate=True, **kwargs ):
            self.proxy_args = kwargs.copy ()
            
            if bool ( self.proxy_args ) is False : # standard way of initalization
                if ':' in (str)(server_address[0]):
                    self.address_family = socket.AF_INET6
                else:
                    self.address_family = socket.AF_INET
                
                self.allow_reuse_address = True
                
                super().__init__ ( server_address, RequestHandlerClass, bind_and_activate )
            else:
                self.proxy_args = kwargs.copy ()
                
                self.RequestHandlerClass = RequestHandlerClass
                
                self.socket = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
                self.server_socket = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
                
                self.server_address = ( "0.0.0.0", 0 )
        
        
        def serve_forever ( self, poll_interval = 0.5 ):
            if  bool ( self.proxy_args ) is False:
                self.socket.settimeout ( poll_interval )
                
                while not test_unittests_stop_event.is_set ():
                    try:
                        self.handle_request ()
                    except OSError:
                        pass
            else:
                d = {}
                
                while True:
                    if ':' in (str)(self.proxy_args [ "proxy_host" ] ):
                        self.proxy_socket = socket.socket ( socket.AF_INET6, socket.SOCK_STREAM )
                    else:
                        self.proxy_socket = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
                    
                    self.proxy_socket.setsockopt ( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
                    
                    self.proxy_socket.settimeout ( 10 )
                    
                    self.proxy_socket.connect ( ( self.proxy_args [ "proxy_host" ], self.proxy_args [ "proxy_port" ] ) )
                    
                    if "username" in self.proxy_args and "password" in self.proxy_args:
                        d [ "username" ] = self.proxy_args [ "username" ]
                        d [ "password" ] = self.proxy_args [ "password" ]
                    
                    server_socks5_client_send_greet ( self.proxy_socket, d )
                    server_socks5_client_recv_choice ( self.proxy_socket, d )
                    
                    server_socks5_client_send_authorization_request ( self.proxy_socket, d )
                    server_socks5_client_recv_authorization_response ( self.proxy_socket, d )
                    
                    d [ "cmd" ] = 2 # bind
                    
                    if "bind.ip.version" in d:
                        d [ "ip.version" ] = d [ "bind.ip.version" ]
                    else:
                        d [ "ip.version" ] = 4
                        
                    if "bind.ip" in d:
                        d [ "ip" ] = d [ "bind.ip" ]
                    else:
                        d [ "ip" ] = "127.0.0.1"
                    
                    if "bind.port" in d:
                        d [ "port" ] = d [ "bind.port" ]
                    else:
                        d [ "port" ] = 0
                    
                    # print ( d )
                    
                    server_socks5_client_send_connect_request ( self.proxy_socket, d )
                    server_socks5_client_recv_connect_response ( self.proxy_socket, d )
                    
                    if "bind.ip" in d:
                        self.bind_host = d [ "bind.ip" ]
                    
                    if "bind.domain" in d:
                        self.bind_host = d [ "bind.domain" ]
                    
                    if "bind.port" in d:
                        self.bind_port = d [ "bind.port" ]
                    
                    print ( f"SOCKS5 Proxy listening on {self.bind_host}:{self.bind_port}" )
                    
                    self.proxy_socket.settimeout ( 18 )
                    
                    rlist, _, _ = select.select ( [ self.proxy_socket, ], [], [] )
                    if self.proxy_socket in rlist:
                        l = {}
                        
                        server_socks5_client_recv_connect_response ( self.proxy_socket, l )
                        
                        address_0 = None
                        address_1 = None
                        
                        if "bind.ip" in l:
                            address_0 = l [ "bind.ip" ]
                        
                        if "bind.domain" in l:
                            address_0 = l [ "bind.domain" ]
                        
                        if "bind.port" in l:
                            address_1 = l [ "bind.port" ]
                        
                        client_address = ( address_0, address_1 )
                        
                        print ( f"Accepted connection from {address_0}:{address_1}" )
                        
                        self.RequestHandlerClass ( self.proxy_socket, client_address, self)
                        self.shutdown_request ( self.proxy_socket )
    
    def server_socks5_client_send_greet ( s : socket.socket, d : dict ):
        # Client greeting   VER NAUTH       AUTH
        # Byte count          1     1   variable
    
        # VER
        #     SOCKS version (0x05)
        # NAUTH
        #     Number of authentication methods supported, uint8
        # AUTH
        #     Authentication methods, 1 byte per method supported
        #     The authentication methods supported are numbered as follows:
    
        #         0x00: No authentication
        #         0x01: GSSAPI (RFC 1961)
        #         0x02: Username/password (RFC 1929)
        #         0x03–0x7F: methods assigned by IANA[17]
        #             0x03: Challenge–Handshake Authentication Protocol
        #             0x04: Unassigned
        #             0x05: Challenge–Response Authentication Method
        #             0x06: Secure Sockets Layer
        #             0x07: NDS Authentication
        #             0x08: Multi-Authentication Framework
        #             0x09: JSON Parameter Block
        #             0x0A–0x7F: Unassigned
        #         0x80–0xFE: methods reserved for private use
    
        methods =  [ 0x00 ]
        
        if "username" in d and "password" in d:
            methods.append ( 0x02 )
            
        transmit = struct.pack ( "BB", 0x05, len ( methods ) )
        transmit += bytes ( methods )
        
        s.sendall ( transmit )
    
    
    def server_socks5_client_recv_choice ( s : socket.socket, d : dict ):
        # Server choice
        #               VER CAUTH
        # Byte count      1     1
    
        # VER
        #     SOCKS version (0x05)
        # CAUTH
        #     chosen authentication method, or 0xFF if no acceptable methods were offered
        
        version, cauth = struct.unpack ( "!BB", s.recv ( 2 ) )
        
        if version != 0x5:
            raise SocksChainException ( "Unsupported socks version", version )
        
        if cauth == 0xff:
            raise AuthenticationChainException ( "No accepted AUTH type" )
        
        d [ "cauth" ] = cauth
    
    
    def server_socks5_client_send_authorization_request ( s : socket.socket, d : dict ):
        #  Client authentication request, 0x02
        #               VER IDLEN       ID  PWLEN       PW
        # Byte count      1     1  (1–255)      1  (1–255)
    
        # VER
        #     0x01 for current version of username/password authentication
        # IDLEN, ID
        #     Username length, uint8; username as bytestring
        # PWLEN, PW
        #     Password length, uint8; password as bytestring
        
        if d [ "cauth" ] == 0x00:
            return
        
        if d [ "cauth" ] != 0x02:
            raise AuthenticationChainException ( "No accepted AUTH type" )
            
        if not "username" in d or not "password" in d:
            raise AuthenticationChainException ( "No AUTH without username and password not accepted" )
        
        transmit = b'\x01' # username/password authentication
        transmit += int.to_bytes ( len ( d [ "username" ] ) )
        transmit += str.encode ( d [ "username" ] )
        
        transmit += int.to_bytes ( len ( d [ "password" ] ) )
        transmit += str.encode ( d [ "password" ] )
        
        s.sendall ( transmit )
    
    
    def server_socks5_client_recv_authorization_response ( s : socket.socket, d : dict ):
        # Server response, 0x02
        #               VER STATUS
        # Byte count      1      1
    
        # VER
        #     0x01 for current version of username/password authentication
        # STATUS
        #     0x00 success, otherwise failure, connection must be closed
        
        if d [ "cauth" ] == 0x00:
            return d
        
        version, status = struct.unpack ( "!BB", s.recv ( 2 ) )
        
        if version != 0x01:
            raise SocksChainException ( "Wrong socks username and password", version )
        
        if status != 0x00:
            raise AuthenticationChainException ( "Access denied", json.dumps ( d ) )
    
    
    def server_socks5_client_send_connect_request ( s : socket.socket, d : dict ):
        # Client connection request
        #             VER CMD RSV  DSTADDR    DSTPORT
        # Byte Count    1   1   1 Variable          2
        
        # VER
        #     SOCKS version (0x05)
        # CMD
        #     command code:
        
        #         0x01: establish a TCP/IP stream connection
        #         0x02: establish a TCP/IP port binding
        #         0x03: associate a UDP port
        
        # RSV
        #     reserved, must be 0x00
        # DSTADDR
        #    SOCKS5 address  TYPE       ADDR
        #    Byte Count         1   variable
        
        #    TYPE
        #        type of the address. One of:
        
        #            0x01: IPv4 address
        #            0x03: Domain name
        #            0x04: IPv6 address
        # DSTPORT
        #     port number in a network byte order
        
        # print ( sys._getframe().f_lineno, d, file=sys.stderr )
        
        transmit = b'\x05'  # SOCKS version 5
        transmit += d [ "cmd" ].to_bytes ()
        transmit += b'\x00'  # Reserved
        
        if "ip.version" in d and d [ "ip.version" ] == 4:  # IPv4
            transmit += b'\x01'
            transmit += socket.inet_pton ( socket.AF_INET, d [ "ip" ] )
        elif "ip.version" in d and d [ "ip.version" ] == 6:  # IPv6
            transmit += b'\x04'
            transmit += socket.inet_pton ( socket.AF_INET6, d [ "ip" ] )
        elif "domain" in d:  # Domain name
            transmit += b'\x03'
            transmit += len ( d [ "domain" ] ).to_bytes ( 1, byteorder='big' )  # Length of domain
            transmit += d [ "domain" ].encode ( 'utf-8' )
        
        transmit += struct.pack('!H', d [ "port" ] )  # Port number
        
        s.sendall ( transmit )
    
    
    def server_socks5_client_recv_connect_response ( s : socket.socket, d : dict ):
        # Response packet from server
        #               VER STATUS  RSV  BNDADDR    BNDPORT
        # Byte Count      1      1    1 variable          2
        
        # VER
        #     SOCKS version (0x05)
        # STATUS
        #     status code:
        
        #         0x00: request granted
        #         0x01: general failure
        #         0x02: connection not allowed by ruleset
        #         0x03: network unreachable
        #         0x04: host unreachable
        #         0x05: connection refused by destination host
        #         0x06: TTL expired
        #         0x07: command not supported / protocol error
        #         0x08: address type not supported
        
        # RSV
        #     reserved, must be 0x00
        # BNDADDR
        #    SOCKS5 address  TYPE       ADDR
        #    Byte Count         1   variable
        
        #    TYPE
        #        type of the address. One of:
        
        #            0x01: IPv4 address
        #            0x03: Domain name
        #            0x04: IPv6 address
        # BNDPORT
        #     server bound port number in a network byte order
        
        version, status, reserved, address_type = struct.unpack("!BBBB", s.recv(4))
        
        # Check for successful response
        
        if version != 0x05:
            raise SocksChainException ( "Unsupported socks version", version )
        
        if status != 0x00:
            raise SocksChainException ( "Socket chain error", status )
            
        # Parse the remaining response based on the address type
        if address_type == 0x01:  # IPv4
            d [ "bind.ip" ] = socket.inet_ntoa ( s.recv ( 4 ) )
            d [ "bind.ip.version" ] = 4
            d [ "bind.port" ] = struct.unpack ( "!H", s.recv ( 2 ) )[ 0 ]
        elif address_type == 0x03:  # Domain name
            domain_len = struct.unpack("!B", s.recv(1))[0]
            d [ "bind.domain" ] = s.recv ( domain_len ).decode ()
            d [ "bind.port" ] = struct.unpack ( "!H", s.recv ( 2 ) )[ 0 ]
        elif address_type == 0x04:  # IPv6
            d [ "bind.ip" ] = socket.inet_ntop ( socket.AF_INET6, s.recv ( 16 ) )
            d [ "bind.ip.version" ] = 6
            d [ "bind.port" ] = struct.unpack ( "!H", s.recv ( 2 ) )[ 0 ]
        else:
            raise Exception("Unknown address type")
    

    Start http-proxy-server:

    test_http_servers_listen_addresses = [
        { "proxy_host": '127.0.0.1', 'proxy_port': 1084 },
        { "proxy_host": '::1', 'proxy_port': 1076, "username": "username2", "password": "password5" },
    ]
    
    for i in test_http_servers_listen_addresses:
        httpd = TestHTTPServerServer ( None, TestHTTPServerHandler, **i )
        
        server_thread = threading.Thread ( target=test_run_http_server, args = ( httpd, ) )
        server_thread.start ()
    

    Tested with gost.