3

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.

  • I have the strong feeling that this question has been here before - what happened to it? – Nico Haase Feb 25 '19 at 11:08
  • Whoever has access to it: this was somehow deleted https://stackoverflow.com/questions/54502266/how-to-use-fsockopen-or-compatible-with-socks-proxies-in-php – Nico Haase Feb 25 '19 at 11:10
  • I explained why in the message, but it was removed by "karel". –  Feb 25 '19 at 12:28
  • So, you think that deleting that old question and posting a new helps? – Nico Haase Feb 25 '19 at 12:39
  • The point is: by deleting it and posting a new question, you lose visibility - you don't gain any. Just keep the question open – Nico Haase Feb 26 '19 at 07:14
  • Maybe that's somehow true, but I thought the opposite: the brief moment when it's "on the first page", it stands a small chance of anyone seeing it... –  Feb 26 '19 at 08:30

1 Answers1

7

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);
}
Community
  • 1
  • 1
t.m.adam
  • 15,106
  • 3
  • 32
  • 52
  • Thanks for your reply. I became genuinely happy since I thought this was a lost cause. However, I can't get your functions to work. With the HTTP proxy function, it immediately outputs: "Request denied, HTTP/1.1 200 Connection established Connection failed, 0," With the SOCKS one, it waits for a bit and then outputs: > USER > NICK < . That last line is the response from the server, and seems to vary slightly each time I try it. But it's always two random(?) characters... Getting "too long by 69 characters", so I can't post the whole response... –  Mar 07 '19 at 08:55
  • I changed your "200 OK" part to "200 Connection established", because that's apparently what it responds with (not sure if that in itself is some bug/non-standard behaviour with the proxy?), and then it "works"... in the sense that it outputs an empty row and closes the connection, without errors this time... But it doesn't work. –  Mar 07 '19 at 08:59
  • Yes, the proper response is `200 Connection established` but some proxy servers may respond `200 OK`, so I've updated the regex to catch both. Both functions were tested with free HTTP proxies from [this list](https://free-proxy-list.net/) and SOCKS5 proxies from [this list](http://spys.one/en/socks-proxy-list/) and work - in most cases, as free proxies are not that reliable. If you can provide more information and code maybe I can help to debug it. – t.m.adam Mar 07 '19 at 15:39
  • It seems that your SOCKS5 proxy server expects some form of authentication, but in this case the error message would be just a non-zero byte. Are you sure that this response doesn't come from the destination host? Have you tried connecting to other hosts or using other proxy servers? – t.m.adam Mar 07 '19 at 16:27
  • The proxies I use (SOCKS5 and HTTP) are the ones provided by my paid VPN. They actually don't have any authentication because they only work if you are already connected to the VPN (so there is no need for passwords). Thus, this can't be the issue. Now that you mention it, I know that some proxies do have username/password instead of doing IP address-based auth, so it would be great if that could also be built in... But right now, I'm stuck at even getting this to work, as you know. I just get empty output from the server and then disconnected. I really don't get why. –  Mar 07 '19 at 16:33
  • The target hostname and port is: chat.freenode.net:6697 ... And as initially mentioned, I'm connected to that with HexChat using the same SOCKS5 proxies that don't work when I use them from PHP, so there must be some sort of issue with the functions. :( –  Mar 07 '19 at 16:34
  • The problem is I've tested both functions with several proxies in the lists I've linked in a previous comment. In most cases they work fine, and they should work as they use a correct communication protocol. Also, the `"USER, NICK"` response probably comes from the IRC server, which means that the SOCKS5 server proxied the request/response, right? – t.m.adam Mar 07 '19 at 17:08
  • Nah... The USER and NICK is sent from the client. As soon as it returns data (none), it disconnects with zero output. –  Mar 07 '19 at 18:12
  • Just to make it clear: I'm still very much stuck and wondering how to solve this. I really appreciate the effort making the reply, but I simply cannot get it to work. Are you able to get chat.freenode.net to respond in any way whatsoever (doesn't even need to connect fully, just get some sort of response)? –  Mar 12 '19 at 23:40
  • No I can't get a response from chat.freenode.net on port 6697 either with or without a proxy, but I tested port 6667 with a SOCKS5 proxy and got a response (are you sure about the port?) There are some things you can do to debug this: How are you communicating with the IRC server, are you using sockets/fsockopen? Can you connect to any HTTP or SOCKS5 proxy (use the free proxies list, just be patient because some of them are down) using the code I provided? Can you communicate with any other IRC servers? I really want to help you and maybe I can if you share your code. – t.m.adam Mar 13 '19 at 00:43
  • Long story short: on https://freenode.net/ , it says: "chat.freenode.net:6697 / non-SSL: 6667". I've discovered that your code *DOES* work, for both the HTTP and SOCKS5 proxy, as long as I use the "non-SSL" port, which means plaintext communication! However, it doesn't work when using the "secure" 6697 port, which I'd really like to be able to. A thousand thanks still, and I'll mark it as "answered" now. If you still have any idea on how to get it to accept encrypted connections, I'd be very interested in hearing it. I have a vague memory of adding "tls://" or "ssl://" in front of something –  Mar 13 '19 at 14:32
  • making it work in other contexts, but I can't get that to work now... (And jeez... this site is so over-engineered with these annoying limits of characters...) –  Mar 13 '19 at 14:32
  • 1
    Glad to hear it worked! I'm afraid SSl is not as simple as using "ssl://", we want an SSL connection with the IRC server, not the proxy server. I'm trying `stream_socket_enable_crypto` and I'll update my answer if I can make it work. – t.m.adam Mar 13 '19 at 17:53
  • Ah. Frustrating regarding the TLS stuff. One minor thing that could be improved that I spotted is that both functions return null when they fail, but fsockopen returns false when it fails, so they should as well in order to be as compatible as possible with that function. Also, would it be very involved to accomplish username/password support for both kinds of proxies? –  Mar 13 '19 at 18:25
  • 1
    Yes I suppose FALSE would be more appropriate in case of failure. We can add basic authentication for HTTP proxies with the `Proxy-Authenticate` header. For SOCKS5 proxies, it seems the USERNAME/PASSWORD code is `0x02` in authentication methods, but the article doesn't mention how to send the USERNAME/PASSWORD values. – t.m.adam Mar 13 '19 at 19:17
  • 1
    Added an example for HTTP proxies with SSL and Basic Authentication. And I managed to get a response from chat.freenode.net:6697 with the new code. Couldn't figure how to use authentication with SOCKS5, but the code for SSL communication is the same as HTTP: use `stream_socket_client` to create the connection, then enable SSL with `stream_socket_enable_crypto`. – t.m.adam Mar 13 '19 at 21:17
  • Thank you again. It took me quite a few hours to figure this out in my code, but now I've added support for TLS and authentication (HTTP proxies only). I could literally not have done it without your help! I really thought that this was going to be one of those many things that just never got resolved at all, but luckily, it wasn't the case this time. –  Mar 15 '19 at 04:31