Problem with WSS handshake (PHP WebSocket Server) - php

I developed a WebSocket server using PHP and it worked fine with ws://, but in production environment it uses https://, then I must use wss://.
Should I use certificate to start the socket or something like that? I can't parse the headers to complete the handshake.
How can I perform handshake behind a https server?
This is a AWS EC2 machine with Amazon Certificate.
I have tried import .pem file to socket initialization, run ws:// behind my https:// environment, and nothing worked :(
Socket initialization:
$socket = stream_socket_server(
"tcp://0.0.0.0:" . env("APP_WSS_PORTA"),
$errno,
$errstr
);
I have tried also:
use Aws\Acm\AcmClient;
$cert = (new AcmClient(include config_path('aws.php')))->GetCertificate([
"CertificateArn" => "arn:aws:acm:sa-east-1:EDITED_TO_STACKOVERFLOW"
])["CertificateChain"];
$cert_path = "cert.pem";
file_put_contents(base_path($cert_path), $cert);
$context = stream_context_create(
["ssl" => ["local_cert"=> $cert_path]]
);
$socket = stream_socket_server(
"tcp://0.0.0.0:" . env("APP_WSS_PORTA"),
$errno,
$errstr,
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
$context
);
My handshake function:
function wsHandshake($data)
{
echo "> Handshake " . remoteIp() . PHP_EOL;
$lines = preg_split("/\r\n/", $data);
$headers = array();
foreach ($lines as $line) {
$line = chop($line);
if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}
var_dump($data); // to debug it :)
if (!isset($headers['Sec-WebSocket-Version']) || $headers['Sec-WebSocket-Version'] < 6) {
echo '> Versao do WebSocket nao suportada' . PHP_EOL;
return false;
}
$sec_accept = base64_encode(pack('H*', sha1($headers['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response .= "Upgrade: websocket\r\n";
$response .= "Connection: Upgrade\r\n";
$response .= "Sec-WebSocket-Accept: " . $sec_accept . "\r\n";
$response .= "\r\n";
return $response;
}
var_dump with ws://
string(448) "GET / HTTP/1.1
Host: 127.0.0.1:3131
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: PuIYHJZ4x8IyXajFf4WAsw==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
"
var_dump with wss://
string(517) "\000\000��}hh�հ�h����`�ݘ����O��GQ�E� S�8�#��,��=��c���C8�ǯ�G!6{<\000$�+�/̨̩�,�0�
� ��\0003\0009\000/\0005\000
\000�\000\000\000�\000\000\000
\000\000
\000\000\000\000\000\000
\000\000\000#\000\000\000\000\000
hhttp/1.1\000\000\000\000\000\000\0003\000k\000i\000\000 ��"�c��GLGX�Ƶ��:�"ŵ�)բ
E��)\000\000Al�d��#Q{��t��q>��eb���u�+�d��M�!2�-��tI����z�y�\ĉ�\000\\000-\000\000\000#\000\0"...

Related

Possible encoding problem using PHP-7 Sockets as WebSocket Server

When trying to use PHP7.3 as SocketServer for WebSockets I encounter a problem where a message send from Firefox come out the socket all messed up. And always differently.
However, messages send from PHP to Firefox are fine.
For example:
socket.onopen = function(e)
{
e.target.send(JSON.stringify({"sessionid" : "5e8a2f30a164e", "sockid" : "5e8a5c8cd99e6"}));
}
one time becomes
)'JR9ZT#GN.Fx+/9JL#
hL+J)/
Z
next time it's
ZXC!+&)1,4<a`m&bj%i9rl=av+,91'xzv?9v9;'c=ux
Clientrequest:
GET / HTTP/1.1
Host: 192.168.5.54:8089
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: */*
Accept-Language: de,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://192.168.5.54
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: uhfFVS5mhYptk6FF8jl73g==
Connection: keep-alive, Upgrade
Cookie: XDEBUG_SESSION=XDEBUG_ECLIPSE
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Servercode[shortend]:
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
$ret = socket_bind($this->socket, $this->host, $this->port);
$ret = socket_listen( $this->socket, 0 );
socket_set_nonblock($this->socket);
while ($this->isServer)
{
$connection = #socket_accept($this->socket);
[... forking for new connection cont in child]
}
Child:
socket_close($this->socket);
[...]
$request = socket_read($connection, 5000); //creates above client request
preg_match('#Sec-WebSocket-Key: (.*)\r\n#', $request, $matches);
$key = base64_encode(pack('H*', sha1($matches[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$headers = "HTTP/1.1 101 Switching Protocols\r\n";
$headers .= "Upgrade: websocket\r\n";
$headers .= "Connection: Upgrade\r\n";
$headers .= "Sec-WebSocket-Version: 13\r\n";
//$headers .= "Sec-WebSocket-Protocol: json\r\n";
$headers .= "Sec-WebSocket-Accept: $key\r\n\r\n";
socket_write($connection, $headers, strlen($headers));
require_once("objs/User.php");
$user = new User();
// Send messages into WebSocket in a loop.
socket_set_nonblock($connection);
while (true)
{
if(($msg = socket_read($connection, 5000)) === "")
{
die;
}
if(!empty($msg))
{
echo $msg."\n";
}
I'm at a loss here and would appreciate your help!
It turns out data send from client->server is always "masked" using WebSockets.
The function unmask($payload) from this post helped me solve the problem.

Parse socket data on a PHP socket_listener

I was able to open a PHP TCP listener socket, but I don't know how to parse the buffer. The client that connects to my socket send a text/html with an additional boundary data with a xml file and an image file.
How can I parse the response to get the XML file on one side and the Image on the other side?
server = socket_create_listen(8086);
socket_getsockname($server, $addr, $port);
if (!$server) {
$message = 'Start TCP socket: Ko. Could not create socket.';
$this->logger->info($message);
die($message);
} else {
$message = 'Start TCP socket: Ok. TCP socket opened on: ' . $addr . ':' . $port . '.';
$this->logger->info($message);
while ($c = socket_accept($server)) {
socket_getpeername($c, $raddr, $rport);
$this->logger->info("Received Connection from $raddr:$rport\n");
$data = '';
while ($bytes = socket_recv($c, $r_data, 1024, MSG_WAITALL)) {
$data .= $r_data;
}
//Edited: Explode with double line and got data
$parsedData = explode("\r\n\r\n", $data);
$xml = new \SimpleXMLElement($parsedData[2]);
print_r($xml);
else {
echo "socket_recv() failed; reason: " . socket_strerror(socket_last_error($c)) . "\n";
}
socket_close($c);
}
}
fclose($server);
This is the output I received:
Received Connection from :59048
Read 3096 bytes from socket_recv(). Closing socket...POST /test HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: zh-CN
Content-Type: multipart/form-data;boundary=-------------------------7e13971310878
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)
Accept-Encoding: gzip, deflate
Host: 0.0.0.0:7200
Content-Length: 516032
Connection: Keep-Alive
Cache-Control: no-cache
---------------------------7e13971310878
Content-Disposition: form-data; name="file.xml";filename="file.xml";
Content-Type: text/xml
Content-Length: 2273
<EventNotificationAlert version="2.0" xmlns="http://www.isapi.org/ver20/XMLSchema">
<ipAddress></ipAddress>
<ipv6Address></ipv6Address>
<portNo></portNo>
<!--REST OF XML DATA-->
</EventNotificationAlert>
---------------------------7e13971310878
Content-Disposition: form-data;name="image.jpg";filename="image.jpg";
Content-Type: image/pjpeg
Content-Length: 7164
����JFIF���
!"$"^C
EDITED: I was able to got the XML data by using "explode" function, but I don't know how to get the 2 images as image files. Any suggestion?
Any help would be really appreciated!
Thank you!
I write my final solution here:
1 - Explode the data in a string to an array(using \r\n\r\n):
$parsedData = explode("\r\n\r\n", $data);
2 - XML is actually on position 2 (position 0 and 1 contain POST HTTP Header and boundary start section):
$xml = new \SimpleXMLElement($parsedData[2]);
Image is on position 4 (array position 3 contains headers such as Content-Disposition that must be skipped):
The code to save the data as an image is:
//Generate GD image from raw string.
if (!$source = #imagecreatefromstring($parsedData[4])){
$message = 'Save Image: Ko. Img path: "' . $imgFullPath . '". Error details: ' . error_get_last()['message'];
$this->logger->error($message);
}else {
//Save GD image on disk
imagejpeg($source, $imgFullPath);
//Clean resources.
imagedestroy($source);
$message = 'Save Image: Ok. Image saved successfully on path: "' . $imgFullPath . '"...';
$this->logger->info($message);
$this->io->writeln($message);

PHP WebSocket Server can not access Origin and Referrer headers

I have a simple PHP websocket server
Here is the full code : https://gist.github.com/hack4mer/e40094001d16c75fe5ae8347ebffccb7
while (true) {
$changed = $clients;
socket_select($changed, $null, $null, 0, 10);
//check for new socket
if (in_array($socket, $changed)) {
$socket_new = socket_accept($socket); //accpet new socket
$clients[] = $socket_new; //add socket to client array
//THIS DOES NOT WORK
print_r($_SERVER);
die();
}
In the browser's network tab, I can confirm the following request:
Request URL: ws://localhost:12345/
Provisional headers are shown
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,hi;q=0.8,ms;q=0.7
Cache-Control: no-cache
Connection: Upgrade
Host: localhost:12345
Origin: http://localhost
However am not able to access these request headers in my script.
My aim is to restrict access of the WebSocket to only few hosts
You will have to buffer the data, at least until the headers are done (CRLF).
Have a look at react/socket.
It is very easy to use non-blocking sockets with that.
To implement the websocket protocol, have a look at ratchetphp/RFC6455.
I solved this issue by doing the header checking before the handshaking.
Full code : https://gist.github.com/hack4mer/e40094001d16c75fe5ae8347ebffccb7
function perform_handshaking($receved_header,$client_conn, $host, $port)
{
$headers = array();
$lines = preg_split("/\r\n/", $receved_header);
foreach($lines as $line)
{
$line = chop($line);
if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
{
$headers[$matches[1]] = $matches[2];
}
}
//HEADERS AVAILABLE HERE -> $headers
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
//hand shaking header
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $host\r\n" .
"WebSocket-Location: ws://$host:$port/demo/shout.php\r\n".
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
socket_write($client_conn,$upgrade,strlen($upgrade));
}

Remote file access from PHP server side gives 301 instead of file, what to do?

EDIT: the answer is in the comments to the marked answer.
I am currently working with updating a few key components on a mobile web site. The site uses data from a different server to display student schedules. Recently this other site (over which I have zero control) was subject to a major overhaul and naturally I now have to update the mobile web site.
What I am trying to do is to access an iCal file and parse it. Since the site I am working on runs in an environment that does not have the curl-library nor have fopen wrappers properly set up I have resorted to the method described here (number 4, using a socket directly).
My current issue is that instead of getting the iCal-file I get a 301 error. However, if I attempt to access the same file (via the same URL) in a web browser it works just fine.
EDIT:
I added a bit of logging and here is what came out of it:
-------------
Querying url:
https://someUrl/schema/ri654Q055ZQZ60QbQ0ygnQ70cWny067Z0109Zx4h0Z7o525Y407Q.ics
Response:
HTTP/1.1 301 Moved Permanently
Server: nginx/1.2.8
Date: Sun, 11 Aug 2013 14:08:36 GMT
Content-Type: text/html
Content-Length: 184
Connection: close
Location:
https://someUrl/schema/ri654Q055ZQZ60QbQ0ygnQ70cWny067Z0109Zx4h0Z7o525Y407Q.ics
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.2.8</center>
</body>
</html>
Redirect url found: https://someUrl/schema/ri654Q055ZQZ60QbQ0ygnQ70cWny067Z0109Zx4h0Z7o525Y407Q.ics
The new location I am getting is identical to the original one.
This is the code used:
function getRemoteFile($url)
{
error_log("------------- \r\nQuerying url: " . $url, 3, "error_log.log");
// get the host name and url path
$parsedUrl = parse_url($url);
$host = $parsedUrl['host'];
if (isset($parsedUrl['path'])) {
$path = $parsedUrl['path'];
} else {
// the url is pointing to the host like http://www.mysite.com
$path = '/';
}
if (isset($parsedUrl['query'])) {
$path .= '?' . $parsedUrl['query'];
}
if (isset($parsedUrl['port'])) {
$port = $parsedUrl['port'];
} else {
// most sites use port 80
// but we want port 443 because we are using https
error_log("Using port 443\r\n" . $url, 3, "error_log.log");
$port = 443;
}
$timeout = 10;
$response = '';
// connect to the remote server
$fp = fsockopen($host, $port, $errno, $errstr, $timeout );
if( !$fp ) {
echo "Cannot retrieve $url";
} else {
$payload = "GET $path HTTP/1.0\r\n" .
"Host: $host\r\n" .
"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3\r\n" .
"Accept: */*\r\n" .
"Accept-Language: sv-SE,sv;q=0.8,en-us,en;q=0.3\r\n" .
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" .
"Referer: https://$host\r\n\r\n";
error_log("\nPAYLOAD: " . $payload, 3, "error_log.log");
// send the necessary headers to get the file
fputs($fp, $payload);
// retrieve the response from the remote server
while ( $line = stream_socket_recvfrom( $fp, 4096 ) ) {
$response .= $line;
}
fclose( $fp );
// naively find location redirect
$location_pos = strpos($response, "Location:");
if($location_pos){
$location_pos += 10;
$new_url = substr($response, $location_pos, strpos($response, "\r\n\r\n") - $location_pos);
error_log("\nRedirect url found: " . $new_url, 3, "error_log.log");
}else{
//log the response
error_log($response, 3, "error_log.log");
}
// strip the headers
$pos = strpos($response, "\r\n\r\n");
$response = substr($response, $pos + 4);
}
// return the file content
return $response;
}
HTTP Response Code 301 is a permanent redirect, not an error.
Your code will have to follow that redirect in order to access the resource.
For example, http://google.com/ returns a 301 in order to redirect users to http://www.google.com/ instead.
$ curl -I http://google.com/
HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Sun, 11 Aug 2013 01:25:34 GMT
Expires: Tue, 10 Sep 2013 01:25:34 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Alternate-Protocol: 80:quic
You can see the 301 response on line 2, followed by the Location header which tells the web browser where to go instead.
What likely happened was that during this major overhaul, they moved the resource to another location. In order not to break any users bookmarks or calendar, they used a 301 redirect so that clients will automatically fetch the resource from the new location.

Implementing handshake for hybi-17

I'm trying to develop the handshake for websocket hybi-17 protocol (https://datatracker.ietf.org/doc/html/draft-ietf-hybi-thewebsocketprotocol-17).
According to that draft, I made the following code for the client (user-agent):
var host = 'ws://localhost/server.php';
if ('MozWebSocket' in window) ws = new MozWebSocket (host);
else ws = new WebSocket (host);
and this code for the server (I skipped the socket initialization/management part):
$key = $value = null;
preg_match ("#Sec-WebSocket-Key: (.*?)\r\n#", $buffer, $match) && $key = $match[1];
$key .= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
$key = sha1 ($key);
$key = pack ('H*', $key);
$key = base64_encode ($key);
$value =
"HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: {$key}";
socket_write ($socket, $value, strlen ($value));
Now, following an example, starting with the client request (simply done with 'new MozWebSocket (host)' call):
GET /server.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: it-it,it;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive, Upgrade
Sec-WebSocket-Version: 8
Sec-WebSocket-Origin: http://localhost
Sec-WebSocket-Extensions: deflate-stream
Sec-WebSocket-Key: oqFCBULD7k+BM41Bc3VEeA==
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
The server response (echoed in the local shell, as a debug line):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: TlKc0Ck7WpqsLhMm/QXABMQWARk=
I followed what specified in the IETF hybi-17 draft but the client request is still pending and there's no real connection between client and server.
What's wrong?
What I have to do more?
Thanks in advance.
A HTTP response is defined as:
Response = Status-Line ; Section 6.1
*(( general-header ; Section 4.5
| response-header ; Section 6.2
| entity-header ) CRLF) ; Section 7.1
CRLF
[ message-body ] ; Section 7.2
The message body is empty, but there should still be two CRLFs after all headers (one CRLF after each header and one final extra one).
So your code should look like:
$value =
"HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: {$key}\r\n\r\n";

Categories