7

I'm trying to send a fire-and-forget request from PHP to my websocket aws api gateway.

I've set up an action called "sendmessage".

This is the code I'm using:

$protocol = "ssl";
$host = "<myendpoint>.amazonaws.com";
$port = 443;
$path = "/<mystage>/";
$timeout = 2000;

$socket = pfsockopen($protocol . "://" . $host, $port,
                    $errno, $errstr, $timeout);

$content = "{'action': 'sendmessage', 'data': 'test'}";
$body = "POST $path HTTP/1.1\r\n";
$body .= "Host: $host\r\n";
$body .= "Content-Type: application/json\r\n";
$body .= "Content-Length: " . strlen($content) . "\r\n";
$body .= "Connection: Close\r\n\r\n";
$body .= $content;
$body .= "\r\n";

fwrite($socket, $body);

However, nothing happens.

If I use wscat, like:

wscat -c wss://<my-endpoint>.amazonaws.com/<my-stage>

> {'action': 'sendmessage', 'data': 'test'}
>

it works just fine.

What am I doing wrong in my php code?

Note: I need the socket connection to be persistent (the way it is when using the pfsockopen function).

OhMad
  • 6,871
  • 20
  • 56
  • 85

4 Answers4

9

Since you didn't provide a handpoint link, here is some notes, following own tests!

I guess the issue comes from the wss part, php needs to retrieve the certificate first, so it can encrypt the data.

Your code should work just fine on a ws:// stream.

To connect to a regular ws:// stream, one can simply use fsockopen().

<?php
$fp = fsockopen("udp://echo.websocket.org", 13, $errno, $errstr);
if (!$fp) {
    echo "ERROR: $errno - $errstr<br />\n";
} else {
    fwrite($fp, "\n");
    echo "Connected!";
    echo fread($fp, 26);
    fclose($fp);
}

But to connect to a wss:// secure websocket stream, using php, without libraries, we need to create a tunnel first, by querying the public key with stream_socket_client.

This is a handshake mechanism. This can be done as follow.

Notice the first ssl:// call. This is the TLS 1.0 protocol.

<?php  
$sock = stream_socket_client("ssl://echo.websocket.org:443",$e,$n,30,STREAM_CLIENT_CONNECT,stream_context_create(null));
if(!$sock){
 echo"[$n]$e".PHP_EOL;
} else {
  fwrite($sock,"GET / HTTP/1.1\r\nHost: echo.websocket.org\r\nAccept: */*\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: ".rand(0,999)."\r\n\r\n");
  while(!feof($sock)){
    var_dump(fgets($sock,2048));
  }
}

The output should looks like:

string(44) "HTTP/1.1 101 Web Socket Protocol Handshake"
string(21) "Connection: Upgrade"
string(37) "Date: Thu, 12 Dec 2019 04:06:27 GMT"
string(52) "Sec-WebSocket-Accept: fTYwcEa6D9kJBtghptkz1e9CtBI="
string(25) "Server: Kaazing Gateway"
string(20) "Upgrade: websocket"

Same base code, another example, pulling data from Binance wss:// stream.

We can also use TLS 1.2, with a tls:// handshake instead. Works on most servers.

<?php
$sock = stream_socket_client("tls://stream.binance.com:9443",$error,$errnum,30,STREAM_CLIENT_CONNECT,stream_context_create(null));
if (!$sock) {
    echo "[$errnum] $error" . PHP_EOL;
} else {
  echo "Connected - Do NOT get rekt!" . PHP_EOL;
  fwrite($sock, "GET /stream?streams=btcusdt@kline_1m HTTP/1.1\r\nHost: stream.binance.com:9443\r\nAccept: */*\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: ".rand(0,999)."\r\n\r\n");
  while (!feof($sock)) {
    var_dump(explode(",",fgets($sock, 512)));
  }
} 

Here is a way to retrieve only the ssl RSA public key of a remote handpoint, from php. Can be used to speed up later connections.

<?php
$opt = [
  "capture_peer_cert" => true,
  "capture_peer_cert_chain" => true
];
$a = stream_context_create(["ssl"=>$opt]);
$b = stream_socket_client("ssl://stream.binance.com:9443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $a);
$cont = stream_context_get_params($b);
$key = openssl_pkey_get_public($cont["options"]["ssl"]["peer_certificate"]);
$c = openssl_pkey_get_details($key);
var_dump($c["key"]);

Output something like:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhki(...)7aEsFtUNkwM5R5b1mpqzAwqHyvdamJx20bT6SS6
PYXSr/dv8ak1d4e2Q0nIa1O7l3w0bZZ4wnp5B8Z+tjPd1W8uaZoRO2iVkPMh2yPl
j0mmtUw1YlfDyutH/t4FlRCDiD4JjdREQGs381/+jbkdjl2SIb1IyNiCdAXA6zsq
xwIDAQAB
-----END PUBLIC KEY-----

There is possibly other quircks, to be sure, we need the main handpoint^. Would be glad to test that. Otherwise good luck, there is a big lack of documentation on the subject.

This is still a new born protocol (2011!). Best details are in the RFC specification:

The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011

About the handshake, it must be initiated by a GET request.

The client will send a pretty standard HTTP request with headers that looks like this (the HTTP version must be 1.1 or greater, and the method must be GET)

Writing_WebSocket_servers#Client_handshake_request


In short:

If unencrypted WebSocket traffic flows through an explicit or a transparent proxy server without WebSockets support, the connection will likely fail.

WebSocket#Proxy_traversal

Transport_Layer_Security#Digital_certificates

Community
  • 1
  • 1
NVRM
  • 11,480
  • 1
  • 88
  • 87
  • 1
    Thanks for the great answer! I got the handshake to work with the GET request you showed and it seemed to connect correctly. How would I now send data over the socket? I tried a POST request (like in my question above) and got a 400 bad request. – OhMad Dec 12 '19 at 09:53
  • The api is just a test I've quickly set up with the standard API Gateway Websockets endpoint. So, no custom docs on my part. And quite frankly, the websocket docs on AWS are horrible. I've been trying to set up another test endpoint to echo back whatever is passed to it... – OhMad Dec 13 '19 at 09:59
1

You probably need to use http_build_query(); function like :

$content = http_build_query($content);

and use form post to send message, so try the following code to check if socket connection is success, probably mistake in your code is pfsockopen() should be @fsockopen()

Edit this for your requirements :

$protocol = "ssl";
$host = "<myendpoint>.amazonaws.com";
$port = 443;
$path = "/<mystage>/";
$timeout = 2000;

$socket = @fsockopen($protocol . "://" . $host, $port,
            $errno, $errstr, $timeout);

if($socket === false) { return false; };

$content = "{'action': 'sendmessage', 'data': 'test'}";
$body  = "POST $path HTTP/1.1\r\n";
$body .= "Host: $host\r\n";
$body .= "Referer: yourClass (v.".version() .")\r\n";
$body .= "Content-type: application/json\r\n";
$body .= "Content-Length: ".strlen($content)."\r\n";
$body .= "Connection: Close\r\n\r\n";
$body .= "$content";
fwrite($socket, $body);
fclose($socket);

This code works fine in my site as a function with

$out .= "Content-type: application/x-www-form-urlencoded\r\n"; instead of json

function flexy_Request($url, $_data) {
    // parse the given URL
    $url = parse_url($url);
    if ($url === false || !isset($url['host']) || !isset($url['path'])) {
        return false;
    }
    // extract host and path:
    $host = $url['host'];
    $path = $url['path'];
    // open a socket connection on port 80
    // use localhost in case of issues with NATs (hairpinning)
    $fp = @fsockopen($host, 80);

    if($fp===false) { return false; };

    $data = http_build_query($_data);
    $out  = "POST $path HTTP/1.1\r\n";
    $out .= "Host: $host\r\n";
    $out .= "Referer: Myclass (v.". flexy_version() .")\r\n";
    $out .= "Content-type: application/json\r\n";
    $out .= "Content-Length: ".strlen($data)."\r\n";
    $out .= "Connection: Close\r\n\r\n";
    $out .= "$data";
    $number_bytes_sent = fwrite($fp, $out);
    fclose($fp);
    return $number_bytes_sent; // or false on fwrite() error
}
0

You need to use PHP websocket client for your requirement. Below is one of the client which can be used for your requirements:

https://github.com/paragi/PHP-websocket-client

Sample 1:

if( $sp = websocket_open('echo.websocket.org',80) ) {
  websocket_write($sp,"hello server");
  echo "Server responed with: " . websocket_read($sp,$errstr);
}

Sample 2:

$headers = ["Cookie: SID=".session_id()];
$sp = websocket_open('echo.websocket.org',80,$headers,$errstr,16);
if($sp){
   $bytes_written = websocket_write($sp,"hello server");
   if($bytes_written){
     $data = websocket_read($sp,$errstr);
     echo "Server responed with: " . $errstr ? $errstr : $data;
   }
}

Hope this helps.

Parth Mehta
  • 1,869
  • 5
  • 15
  • Thanks for the answer. How would I modify the example to send json data to a specific path? Also, will the connection that's opened be persisted while running the process (like with pfsockopen)? – OhMad Dec 07 '19 at 13:19
  • I believe you just append the path to echo.websocket.org/my-path/. The above library uses stream_socket_client instead of pfsockopen. This link compares the two methods: http://www.spudsdesign.com/benchmark/index.php?t=socket1 – Parth Mehta Dec 07 '19 at 13:40
  • When I try to append the path, it gives me a "Name or service not known" error. – OhMad Dec 08 '19 at 08:33
  • 1
    I think you are right, also the project documentation is a little unclear. If you are open to give another library a try, I'd suggest this tool which seems to have slightly better documentation: https://github.com/arthurkushman/php-wss/blob/bf80569bcad8a360d2541006ffb244a16adf2575/README.md – Parth Mehta Dec 08 '19 at 09:22
  • Thank you, that library works a lot better! But do you know if the WebSocketClient library will reuse an already established connection? – OhMad Dec 08 '19 at 10:04
  • You will have to do this at the server side, when client initiates the connection, the server can choose to return an already established connection instead of creating a new one. Client does not need to deal with this. – Parth Mehta Dec 08 '19 at 10:20
  • Isn't that what the pfsockopen function would already do? The rationale was to avoid another roundtrip to the server if there's a way to keep the connection. – OhMad Dec 08 '19 at 10:23
  • Yes you are right, if server uses pfsockopen then yes it should take care of it. – Parth Mehta Dec 08 '19 at 10:38
  • I found out that you can pass STREAM_CLIENT_PERSISTENT to the stream_socket_client function, but if I pass that into the above library instead of STREAM_CLIENT_CONNECT (which it uses by default), it unfortunately breaks. – OhMad Dec 08 '19 at 13:04
  • it might be possible to rearchitect the plugin to use persistent connection but it appears it is not yet supported. That said STREAM_CLIENT_PERSISTENT is just a flag so should not require a big effort to refactor. Might be worth measuring and checking if latency is acceptable as it is. – Parth Mehta Dec 08 '19 at 13:56
-2

I would recommend using the cURL extension for PHP. The cURL lib maintains a pool of persistent connections by default.

<?php
$proto     = 'https';
$host      = 'FQDN';         
$path      = '/path/to/file';

$post_data_array = array(
 'action'  => 'sendmessage',
 'data'    => 'test',
);

$payload = json_encode($post_data_array);

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $proto.'://'.$host.$path);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($payload),
    'Connection: Keep-Alive',
    'Keep-Alive: 300',
  )
);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

curl_exec($ch);           

curl_close($ch);
  • Thank you for the answer. The endpoint is giving me a 403 forbidden error. How can I investigate what's wrong? – OhMad Dec 10 '19 at 09:00
  • Please disregard my reply to your comment. It was not correct. To address this new issue, I want to refer you to a different SO post: https://stackoverflow.com/questions/18447454/apache-giving-403-forbidden-errors If that does not help resolve your issue, I would suggest opening a new question with the specific details of the new issue. – Jim Lucas Dec 11 '19 at 08:08