Search code examples
phpwindowsproxyircfsockopen

How to use fsockopen (or compatible) with SOCKS proxies in PHP?


I've coded a non-evil, non-spammy IRC bot in PHP, using fsockopen and related functions. It works. However, the problem is that I need to support proxies (preferably SOCKS5, but HTTP is also OK if that is somehow easier, which I doubt). This is not supported by fsockopen.

I've gone through all search results for "PHP fsockopen proxy" and related queries. I know of all the things that don't work, so please don't link to one of them.

The PHP manual page for fsockopen mentions the function stream_socket_client() as

similar but provides a richer set of options, including non-blocking connection and the ability to provide a stream context.

This sounded promising at first, supposedly allowing me to just replace the fsockopen call with stream_socket_client and specify a proxy, maybe via a "stream context"... but it doesn't. Or does it? I'm very confused by the manual.

Please note that it must be a PHP code solution; I cannot pay for "Proxifier" or use any other external software to "wrap around" this.

All the things I've tried seem to always result in me getting a bunch of empty output from the server, and then the socket is forcefully closed. Note that the proxy I'm trying with works when I use HexChat (a normal IRC client), with the same network, so it's not the proxies themselves that are at fault.


Solution

  • As far as I know there is no default option to set a SOCKS or HTTP proxy for fsockopen or stream_socket_client (we could create a context and set a proxy in HTTP options, but that doesn't apply to stream_socket_client). However we can establish a connection manually.

    Connecting to HTTP proxies is quite simple:

    • The client connects to the proxy server and submits a CONNECT request.
    • The server responds 200 if the request is accepted.
    • The server then proxies all requests between the client and destination host.

    <!- -!>

    function connect_to_http_proxy($host, $port, $destination) {
        $fp = fsockopen($host, $port, $errno, $errstr);
        if ($errno == 0) {
            $connect = "CONNECT $destination HTTP/1.1\r\n\r\n";
            fwrite($fp, $connect);
            $rsp = fread($fp, 1024);
            if (preg_match('/^HTTP\/\d\.\d 200/', $rsp) == 1) {
                return $fp;
            }
            echo "Request denied, $rsp\n";
            return false;
        }
        echo "Connection failed, $errno, $errstr\n";
        return false;
    }
    

    This function returns a file pointer resource if the connection is successful, else FALSE. We can use that resource to communicate with the destination host.

    $proxy = "138.204.48.233";
    $port = 8080;
    $destination = "api.ipify.org:80";
    $fp = connect_to_http_proxy($proxy, $port, $destination);
    if ($fp) {
        fwrite($fp, "GET /?format=json HTTP/1.1\r\nHost: $destination\r\n\r\n");
        echo fread($fp, 1024);
        fclose($fp);
    }
    

    The communication protocol for SOCKS5 proxies is a little more complex:

    • The client connects to the proxy server and sends (at least) three bytes: The first byte is the SOCKS version, the second is the number of authentication methods, the next byte(s) is the authentication method(s).
    • The server responds with two bytes, the SOCKS version and the selected authentication method.
    • The client requests a connection to the destination host. The request contains the SOCKS version, followed by the command (CONNECT in this case), followed by a null byte. The fourth byte specifies the address type, and is followed by the address and port.
    • The server finally sends ten bytes (or seven or twenty-two, depending on the destination address type). The second byte contains the status and it should be zero, if the request is successful.
    • The server proxies all requests.

    <!- -!>

    More details: SOCKS Protocol Version 5.

    function connect_to_socks5_proxy($host, $port, $destination) {
        $fp = fsockopen($host, $port, $errno, $errstr);
        if ($errno == 0) {
            fwrite($fp, "\05\01\00");
            $rsp = fread($fp, 2);
            if ($rsp === "\05\00" ) {
                list($host, $port) = explode(":", $destination);
                $host = gethostbyname($host); //not required if $host is an IP
                $req = "\05\01\00\01" . inet_pton($host) . pack("n", $port);
                fwrite($fp, $req);
                $rsp = fread($fp, 10);
                if ($rsp[1] === "\00") {
                    return $fp;
                }
                echo "Request denied, status: " . ord($rsp[1]) . "\n";
                return false;
            } 
            echo "Request denied\n";
            return false;
        }
        echo "Connection failed, $errno, $errstr\n";
        return false;
    }
    

    This function works the same way as connect_to_http_proxy. Although both functions are tested, it would be best to use a library; the code is provided mostly for educational purposes.


    SSL support and authentication.

    We can't create an SSL connection with fsockopen using the ssl:// or tls:// protocol, because that would attempt to create an SSL connection with the proxy server, not the destination host. But it is possible to enable SSL with stream_socket_enable_crypto and create a secure communication channel with the destination, after the connenection with the proxy server has been established. This requires to disable peer verification, which can be done with stream_socket_client using a custom context. Note that disabling peer verification may be a security issue.

    For HTTP proxies we can add authentication with the Proxy-Authenticate header. The value of this header is the authentication type, followed by the username and password, base64 encoded (Basic Authentication).

    For SOCKS5 proxies the authentication process is - again - more complex. It seems we have to change the authentication code fron 0x00 (NO AUTHENTICATION REQUIRED) to 0x02 (USERNAME/PASSWORD authentication). It is not clear to me how to create a request with the authentication values, so I can not provide an example.

    function connect_to_http_proxy($host, $port, $destination, $creds=null) {
        $context = stream_context_create(
            ['ssl'=> ['verify_peer'=> false, 'verify_peer_name'=> false]]
        );
        $soc = stream_socket_client(
            "tcp://$host:$port", $errno, $errstr, 20, 
            STREAM_CLIENT_CONNECT, $context
        );
        if ($errno == 0) {
            $auth = $creds ? "Proxy-Authorization: Basic ".base64_encode($creds)."\r\n": "";
            $connect = "CONNECT $destination HTTP/1.1\r\n$auth\r\n";
            fwrite($soc, $connect);
            $rsp = fread($soc, 1024);
            if (preg_match('/^HTTP\/\d\.\d 200/', $rsp) == 1) {
                return $soc;
            }
            echo "Request denied, $rsp\n";
            return false;
        }
        echo "Connection failed, $errno, $errstr\n";
        return false;
    }
    
    $host = "proxy IP";
    $port = "proxy port";
    $destination = "chat.freenode.net:6697";
    $credentials = "user:pass";
    $soc = connect_to_http_proxy($host, $port, $destination, $credentials);
    if ($soc) {
        stream_socket_enable_crypto($soc, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
        fwrite($soc,"USER test\nNICK test\n");
        echo fread($soc, 1024);
        fclose($soc);
    }