├── .gitignore ├── phpClient ├── out1.JPG ├── testWithPHPSockets.php ├── simulateBackend.php ├── README.md ├── websocketPhp.php └── websocketCore.php ├── webClient ├── out1.JPG ├── out2.JPG ├── out3.JPG ├── README.md ├── testWithWebSocket.php ├── startWebClient.js └── socketWebClient.js ├── config ├── websock.ini └── websock_example.ini ├── server ├── websocketserver.service ├── errorHandler.php ├── getOptions.php ├── resource.php ├── resourceWeb.php ├── resourcePHP.php ├── resourceDefault.php ├── runSocketServer.php ├── logToFile.php ├── README.md ├── RFC_6455.php └── webSocketServer.php ├── php2php ├── README.md ├── sender.php └── receiver.php ├── include ├── README.md ├── adressPort.inc.php └── adressPort.example.inc.php ├── websocketExtern ├── README.md ├── websocketPie.php ├── websocketBinance.php ├── websocketHuobi.php ├── websocketOkx.php ├── websocketxrpl.php └── websocketOrg.php ├── LICENSE.md ├── autoload ├── classmap.php └── classLoader.php ├── statistics.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ 2 | /obsolete/ 3 | /monitor/ 4 | /ignore/ 5 | /test/ 6 | -------------------------------------------------------------------------------- /phpClient/out1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/napengam/phpWebSocketServer/HEAD/phpClient/out1.JPG -------------------------------------------------------------------------------- /webClient/out1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/napengam/phpWebSocketServer/HEAD/webClient/out1.JPG -------------------------------------------------------------------------------- /webClient/out2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/napengam/phpWebSocketServer/HEAD/webClient/out2.JPG -------------------------------------------------------------------------------- /webClient/out3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/napengam/phpWebSocketServer/HEAD/webClient/out3.JPG -------------------------------------------------------------------------------- /config/websock.ini: -------------------------------------------------------------------------------- 1 | adress='ws://localhost:8094' 2 | logfile=D:/temp/websock.log 3 | console=false 4 | certFile='' 5 | pkFile='' 6 | 7 | -------------------------------------------------------------------------------- /config/websock_example.ini: -------------------------------------------------------------------------------- 1 | adress='ws://localhost:8094' 2 | logfile=D:/temp/websock.log 3 | console=false 4 | certFile='' 5 | pkFile='' 6 | 7 | -------------------------------------------------------------------------------- /server/websocketserver.service: -------------------------------------------------------------------------------- 1 | [unit] 2 | Description=websocketservice 3 | After=network.target 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/bin/php /var/www/html/phpWebSocketServer/server/runSocketServer.php 7 | [Install] 8 | WantedBy=multi-user.target 9 | -------------------------------------------------------------------------------- /php2php/README.md: -------------------------------------------------------------------------------- 1 | # Have a php script receive feedback from any other script 2 | 3 | Start script receiver.php first on console or browser. 4 | 5 | 6 | Using new parameter $ident when connecting to server. 7 | This way we can receive messages from client who now our $ident. 8 | 9 | ### receiver.php 10 | 11 | Sending messages to receiver using new parameter $ident 12 | 13 | ### sender.php 14 | 15 | -------------------------------------------------------------------------------- /include/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## adressPort.inc.php 3 | 4 | Holds adress of the server 5 | Used only by php/web testclients 6 | 7 | NOTE: 8 | Specify the adress like. 9 | 15 | 16 | If no port is given default is 443 fro ssl:// wss:// 17 | adn 80 for ws:// tcp:// 18 | 19 | -------------------------------------------------------------------------------- /server/errorHandler.php: -------------------------------------------------------------------------------- 1 | log("errno: $errno ; $errstr in file $errfile , Line: $errline"); 16 | return true; 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /websocketExtern/README.md: -------------------------------------------------------------------------------- 1 | # Connecting to other external websocke servers 2 | 3 | 4 | This files are to proof that the handshake performed by a php client is correct. 5 | You can use these as a examples how to use the classe given in `../phpClient` 6 | 7 | 8 | 9 | ## websocketOrg.php 10 | 11 | This connects to https://websocket.org/echo.html 12 | You will see ***Hello from php*** in the browser 13 | 14 | 15 | ## websocketPie.php 16 | 17 | This script connects to https://www.piesocket.com/websocket-tester 18 | ***{"error":"Domain not allowed for this api key"}*** -------------------------------------------------------------------------------- /websocketExtern/websocketPie.php: -------------------------------------------------------------------------------- 1 | readSocket(); 15 | echo $respo; 16 | } 17 | 18 | } 19 | 20 | $x = new websocketPie("wss://demo.piesocket.com/v3/1?api_key=oCdCMcMPQpbvNjUIzqtvF1d2X2okWpDQj4AwARJuAgtjhzKxVEjQU6IdCjwm"); 21 | 22 | -------------------------------------------------------------------------------- /php2php/sender.php: -------------------------------------------------------------------------------- 1 | feedback("Hello 1 from sender", 'receiver'); 15 | $talk->feedback("Hello 22 from sender", 'receiver'); 16 | $talk->feedback("Hello 333 from sender", 'receiver'); 17 | -------------------------------------------------------------------------------- /websocketExtern/websocketBinance.php: -------------------------------------------------------------------------------- 1 | readSocket(); 17 | echo var_dump(json_decode($respo)); 18 | } 19 | 20 | } 21 | 22 | $x = new websocketBinance("wss://stream.binance.com:9443/ws/btcusdt@ticker"); 23 | 24 | -------------------------------------------------------------------------------- /include/adressPort.inc.php: -------------------------------------------------------------------------------- 1 | readSocket(); // read will wait for data 20 | echo "$msg
"; 21 | ob_flush(); 22 | flush(); 23 | } -------------------------------------------------------------------------------- /websocketExtern/websocketHuobi.php: -------------------------------------------------------------------------------- 1 | writeSocket('{"op": "sub","cid": "id generated by client","topic": "public.$service.heartbeat"}'); 18 | $respo = $this->readSocket(); 19 | $xx=zlib_decode($respo); 20 | echo var_dump(json_decode($xx)); 21 | } 22 | 23 | } 24 | 25 | $x = new websocketHuobi("wss://api.hbdm.com/center-notification"); 26 | 27 | -------------------------------------------------------------------------------- /websocketExtern/websocketOkx.php: -------------------------------------------------------------------------------- 1 | writeSocket($InitialTXLookup); 32 | 33 | $resp1 = $this->readSocket(); 34 | echo $resp1; 35 | } 36 | 37 | } 38 | 39 | $x = new websocketOkx("wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999"); 40 | 41 | -------------------------------------------------------------------------------- /websocketExtern/websocketxrpl.php: -------------------------------------------------------------------------------- 1 | writeSocket(' {"command": "server_info"} '); 13 | // $resp1 = $this->readSocket(); 14 | //secho $resp1; 15 | $InitialTXLookup = json_encode(array( 16 | 'id' => 2, 17 | 'command' => "account_tx", 18 | 'account' => "r4DymtkgUAh2wqRxVfdd3Xtswzim6eC6c5", 19 | 'ledger_index_min' => -1, 20 | 'ledger_index_max' => -1, 21 | 'binary' => false, 22 | 'limit' => 50, 23 | 'forward' => true 24 | )); 25 | 26 | $this->writeSocket($InitialTXLookup); 27 | 28 | $resp1 = $this->readSocket(); 29 | echo $resp1; 30 | 31 | } 32 | 33 | } 34 | 35 | $x = new websocketXrpl("wss://xrplcluster.com"); 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 napengam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /phpClient/testWithPHPSockets.php: -------------------------------------------------------------------------------- 1 | 1) { 4 | parse_str(implode('&', array_slice($argv, 1)), $_GET); 5 | } 6 | 7 | require __DIR__ . '/../autoload/classLoader.php'; 8 | 9 | include '../include/adressPort.inc.php'; 10 | $talk = new websocketPhp($Address . '/php'); 11 | if (!isset($_GET['m'])) { 12 | $_GET['m'] = ''; 13 | } 14 | 15 | 16 | $message = trim($_GET['m']); 17 | if ($message == '') { 18 | //return; 19 | $message = 'hallo from PHP'; 20 | } else { 21 | $talk->broadcast($message); 22 | $talk->silent(); 23 | exit; 24 | } 25 | $longString = ''; 26 | for ($i = 0; $i < 9 * 1024; $i++) { 27 | $longString .= 'P'; 28 | } 29 | 30 | /* 31 | * *********************************************** 32 | * test if messages apear in same order as send 33 | * no message is lost and very long message is buffered 34 | * *********************************************** 35 | */ 36 | 37 | $talk->broadcast("$message 1"); 38 | $talk->broadcast("$message 2"); 39 | $talk->broadcast("$message 3"); 40 | $talk->broadcast("$message 4"); 41 | $talk->broadcast("$message 5"); 42 | $talk->broadcast("äüöÄÜÖß$longString 6~6~6~6 ÄÜÖßäüö"); 43 | $talk->broadcast("$message 6"); 44 | 45 | $talk->silent(); 46 | 47 | -------------------------------------------------------------------------------- /phpClient/simulateBackend.php: -------------------------------------------------------------------------------- 1 | uuid = $payload->uuid; 19 | /* 20 | * *********************************************** 21 | * send feedback to client 22 | * *********************************************** 23 | */ 24 | 25 | $talk->feedback("doing some work sleep(1)"); 26 | sleep(1); // work 27 | $talk->feedback("very importand work sleep(2)"); 28 | sleep(2); // work 29 | for ($i = 0; $i < 1000000; $i++) { 30 | if ($i % 1000 == 0) { 31 | $talk->feedback("loop $i"); 32 | } 33 | } 34 | $talk->feedback("done"); 35 | $talk->silent(); 36 | /* 37 | * *********************************************** 38 | * end of AJAX call 39 | * *********************************************** 40 | */ 41 | echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); 42 | 43 | -------------------------------------------------------------------------------- /autoload/classmap.php: -------------------------------------------------------------------------------- 1 | 'F:/xampp-htdocs/phpWebSocketServer/server/errorHandler.php', 8 | 'getOptions' => 'F:/xampp-htdocs/phpWebSocketServer/server/getOptions.php', 9 | 'logToFile' => 'F:/xampp-htdocs/phpWebSocketServer/server/logToFile.php', 10 | 'resource' => 'F:/xampp-htdocs/phpWebSocketServer/server/resource.php', 11 | 'resourceDefault' => 'F:/xampp-htdocs/phpWebSocketServer/server/resourceDefault.php', 12 | 'resourcePHP' => 'F:/xampp-htdocs/phpWebSocketServer/server/resourcePHP.php', 13 | 'resourceWeb' => 'F:/xampp-htdocs/phpWebSocketServer/server/resourceWeb.php', 14 | 'RFC_6455' => 'F:/xampp-htdocs/phpWebSocketServer/server/RFC_6455.php', 15 | 'runSocketServer' => 'F:/xampp-htdocs/phpWebSocketServer/server/runSocketServer.php', 16 | 'webSocketServer' => 'F:/xampp-htdocs/phpWebSocketServer/server/webSocketServer.php', 17 | 'simulateBackend' => 'F:/xampp-htdocs/phpWebSocketServer/phpClient/simulateBackend.php', 18 | 'testWithPHPSockets' => 'F:/xampp-htdocs/phpWebSocketServer/phpClient/testWithPHPSockets.php', 19 | 'websocketCore' => 'F:/xampp-htdocs/phpWebSocketServer/phpClient/websocketCore.php', 20 | 'websocketPhp' => 'F:/xampp-htdocs/phpWebSocketServer/phpClient/websocketPhp.php', 21 | ); 22 | -------------------------------------------------------------------------------- /websocketExtern/websocketOrg.php: -------------------------------------------------------------------------------- 1 | finBit = false; // turn fragmenting on 21 | 22 | $this->writeSocket("Hello"); //first fragment 23 | 24 | $this->writeSocket(" from "); // next fragment 25 | 26 | $this->finBit = true; // last fragment, turn off now 27 | $this->writeSocket("PHP fragmented"); 28 | 29 | $respo = $this->readSocket(); 30 | /* 31 | * *********************************************** 32 | * not fragmented 33 | * *********************************************** 34 | */ 35 | echo $respo; 36 | $this->writeSocket(" Hallo from PHP not fragmented"); 37 | $respo = $this->readSocket(); 38 | echo "
$respo"; 39 | } 40 | 41 | } 42 | 43 | $x = new websocketOrg("wss://echo.websocket.events"); 44 | 45 | -------------------------------------------------------------------------------- /server/getOptions.php: -------------------------------------------------------------------------------- 1 | getOptArgv($expected); 9 | 10 | $iniFile = $cliOptions['i'] ?? $defaultIni; 11 | if (!file_exists($iniFile)) { 12 | throw new RuntimeException("Config file not found: $iniFile"); 13 | } 14 | $ini = parse_ini_file($iniFile, false, INI_SCANNER_TYPED); 15 | 16 | if ($ini === false) { 17 | throw new RuntimeException("Failed to parse ini file: $iniFile"); 18 | } 19 | 20 | // Merge CLI overrides on top of ini config 21 | $this->config = array_replace($ini, $cliOptions); 22 | } 23 | 24 | public function getConfig(): array { 25 | return $this->config; 26 | } 27 | 28 | private function getOptArgv(array $expect): array { 29 | global $argv, $argc; 30 | $out = []; 31 | 32 | for ($i = 1; $i < $argc; $i++) { 33 | if (!in_array($argv[$i], $expect, true)) { 34 | continue; 35 | } 36 | 37 | $exp = ltrim($argv[$i], '-'); 38 | 39 | if ($i + 1 < $argc && $argv[$i + 1][0] !== '-') { 40 | $i++; 41 | $out[$exp] = $argv[$i]; 42 | } else { 43 | $out[$exp] = true; // boolean flag 44 | } 45 | } 46 | 47 | return $out; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /webClient/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Example 3 | 4 | 5 | ## socketWebClient.js 6 | 7 | Use this JavaScript to connect to the server. 8 | 9 | The server sends JSON encoded messages in the form of 10 | `{'opcode':code, /*messag depeding on opcode*/}` 11 | Opcodes that are handled inside this script. 12 | - ready 13 | - next 14 | - close 15 | 16 | **Note** every message send to the server is acknowledged by the server 17 | with an opcode 'next'. 18 | 19 | This script handles my required dialog right after the handshake is done and the connection 20 | is established. 21 | 22 | - the server sends opcode 'ready' and a UUID to identify this client 23 | With this UUID the client is registered with the server 24 | - The serve sends a message with opcode 'next' 25 | signaling that the client can send the next message. 26 | 27 | 28 | This script also sends very long messages in chunks of `chunksize=6 * 1204`. 29 | 30 | 31 | 32 | ## testWithWebSocket.php 33 | 34 | Example to SHOW `broadcast` from other web clients and `feedback` from 35 | a backend script. 36 | 37 | Have the server started and waiting .... 38 | 39 | **Step 1** 40 | 41 | In a browser window open `http://your.web.server/testWithWebSocket.php`. 42 | You will see this. 43 | 44 | ![webApp](out1.JPG) 45 | 46 | **Step 2** 47 | 48 | In an other browser window open `http://your.web.server/testWithWebSocket.php`. 49 | This will broadcast into the page you opened in **Step 1**. 50 | Press the button ***Talk to others*** then you will see the output below 51 | 52 | ![webApp](out2.JPG) 53 | 54 | 55 | Press button ***CALL Backend via AJAX*** then ypu will see ouput form the 56 | backend script `../phpClient/simulateBackend.php` flowing in. 57 | 58 | ![webApp](out3.JPG) -------------------------------------------------------------------------------- /statistics.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Path ; $path

"; 10 | echo 'Number of Files
'; 11 | echo var_dump($numfiles); 12 | echo 'Number of Lines of Code
'; 13 | echo var_dump($numlines); 14 | echo 'Size in KBytes
'; 15 | echo var_dump($numsize); 16 | exit; 17 | ?> 18 | 19 | 20 | 0) { 45 | readSourceFile($path . '/' . $file, $ext); 46 | } 47 | } 48 | closedir($dh); 49 | return; 50 | } 51 | } 52 | ?> 53 | 54 | 55 | -------------------------------------------------------------------------------- /webClient/testWithWebSocket.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebClient 7 | 8 | 9 | Status: not connected

10 | 11 |

12 | 13 |

14 | Here you will see feedback from backend : 15 |

16 |

17 | Here you will see echo : 18 |

19 | 22 |


23 | 24 |
25 | Messages from other web clients:
26 |
27 | " 35 | . "server='$Address';" 36 | . "port='$Port';" 37 | . ""; 38 | ?> 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /phpClient/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | ## simulateBackend.php 4 | 5 | A PHP script that simulates a backend script called via AJAX from a web client 6 | to demonstrate `feedback` from backend. 7 | 8 | ## websocketCore.php 9 | 10 | Base clase to be extended. Impements handshake encode, write decode, read. 11 | With this you can connect to any websocket server,. See directory ***websocketOrg*** 12 | 13 | ## websocketPhp.php 14 | 15 | Class that extends ***websocketCore*** , implemets dialog with this websocket server. 16 | Same functionality like ***socketPhpClient.php*** 17 | 18 | 19 | ## testWithPHPSockets.php 20 | 21 | Have the server started and waiting .... 22 | 23 | 24 | in the command window where you startet the server you should see some output 25 | like this 26 | 27 | >***php runSocketServer.php -console*** 28 | 29 | >Wed, 14 Jul 2021 08:38:41 +0200; Server initialized on WINNT localhost:8091 30 | >Wed, 14 Jul 2021 08:38:41 +0200; Starting server... 31 | >Wed, 14 Jul 2021 08:38:41 +0200; Registered resource : / 32 | >Wed, 14 Jul 2021 08:38:41 +0200; Registered resource : /web 33 | >Wed, 14 Jul 2021 08:38:41 +0200; Registered resource : /php 34 | > 35 | 36 | > ***php testWithPHPSockets.php*** 37 | 38 | >Wed, 14 Jul 2021 08:41:12 +0200; Connecting from IP: ::1 39 | >Wed, 14 Jul 2021 08:41:12 +0200; New client connecting from [::1]:64265 on socket #20 40 | > 41 | >Wed, 14 Jul 2021 08:41:12 +0200; Handshake: /phpClient 42 | >Wed, 14 Jul 2021 08:41:12 +0200; ClientType:tcp 43 | >Wed, 14 Jul 2021 08:41:12 +0200; Telling Client to start on #20 44 | >Wed, 14 Jul 2021 08:41:12 +0200; Buffering ON 45 | >Wed, 14 Jul 2021 08:41:12 +0200; Buffering OFF 46 | >Wed, 14 Jul 2021 08:41:12 +0200; Socket 20 - Client disconnected by Server - TCP connection lost 47 | >Wed, 14 Jul 2021 08:41:12 +0200; Connection closed to socket #20 48 | 49 | If you have a web client open and running in a browser you should 50 | see the messages also in the web client window. 51 | 52 | 53 | ![in browser](out1.JPG) -------------------------------------------------------------------------------- /server/resource.php: -------------------------------------------------------------------------------- 1 | broadCastS, $SocketID, $M); 17 | } 18 | 19 | final function feedback($packet) { 20 | call_user_func($this->feedbackS, $packet); 21 | } 22 | 23 | final function echo($sockid, $packet) { 24 | call_user_func($this->echoS, $sockid, $packet); 25 | } 26 | 27 | final function Log($m) { 28 | call_user_func($this->LogS, $m); 29 | } 30 | 31 | final function Close($SocketID) { 32 | call_user_func($this->CloseS, $SocketID); 33 | } 34 | 35 | function onOpen($SocketID) { 36 | 37 | } 38 | 39 | function onData($SocketID, $M) { //... a message from client 40 | } 41 | 42 | function onClose($SocketID) { // ...socket has been closed AND deleted 43 | } 44 | 45 | function onError($SocketID, $M) { // ...any connection-releated error 46 | } 47 | 48 | final public function registerServerMethods($server) { 49 | /* 50 | * *********************************************** 51 | * extract methods from server neede in clients 52 | * ********************************************** 53 | */ 54 | foreach ($this->methods as $index => $meth) { 55 | $this->{$this->methods[$index] . 'S'} = [$server, $meth]; 56 | } 57 | } 58 | 59 | final function getPacket($M) { 60 | $packet = json_decode($M); 61 | $err = json_last_error(); 62 | if ($err) { 63 | $packet = (object) ['opcode' => 'jsonerror', 'message' => $err]; 64 | } 65 | return $packet; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server/resourceWeb.php: -------------------------------------------------------------------------------- 1 | } 18 | * This is just an example used here , you can send what ever you want. 19 | * ***************************************** 20 | */ 21 | 22 | 23 | $packet = $this->getPacket($M); 24 | $this->packet = $packet; 25 | 26 | if ($packet->opcode === 'jsonerror') { 27 | $this->Log("jsonerror closing #$SocketID"); 28 | $this->Close($SocketID); 29 | return; 30 | } 31 | if ($packet->opcode === 'quit') { 32 | /* 33 | * ***************************************** 34 | * client quits 35 | * ***************************************** 36 | */ 37 | $this->Log("QUIT; Connection closed to socket #$SocketID"); 38 | $this->Close($SocketID); 39 | return; 40 | } 41 | if ($packet->opcode === 'feedback') { 42 | /* 43 | * ***************************************** 44 | * send feedback to client with uuid found 45 | * in $packet 46 | * ***************************************** 47 | */ 48 | $this->feedback($packet); 49 | return; 50 | } 51 | if ($packet->opcode === 'echo') { 52 | $this->echo($SocketID, $packet); 53 | return; 54 | } 55 | if ($packet->opcode === 'broadcast') { 56 | $this->broadCast($SocketID, $M); 57 | return; 58 | } 59 | /* 60 | * ***************************************** 61 | * unknown opcode-> do nothing or close socket 62 | * ***************************************** 63 | * $this->server>close($SocketID); 64 | * 65 | */ 66 | } 67 | 68 | function onError($SocketID, $M) { 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/resourcePHP.php: -------------------------------------------------------------------------------- 1 | } 18 | * Thsi is just an example used here , you can send what ever you want. 19 | * ***************************************** 20 | */ 21 | $packet = $this->getPacket($M); 22 | 23 | if ($packet->opcode === 'jsonerror') { 24 | $this->Log("jsonerror closing #$SocketID"); 25 | $this->Close($SocketID); 26 | return; 27 | } 28 | 29 | if ($packet->opcode === 'quit') { 30 | /* 31 | * ***************************************** 32 | * client quits 33 | * ***************************************** 34 | */ 35 | $this->Log("QUIT; Connection closed to socket #$SocketID"); 36 | $this->Close($SocketID); 37 | return; 38 | } 39 | 40 | if ($packet->opcode === 'feedback') { 41 | /* 42 | * ***************************************** 43 | * send feedback to client with uuid found 44 | * $packet 45 | * ***************************************** 46 | */ 47 | $this->feedback($packet); 48 | return; 49 | } 50 | if ($packet->opcode === 'echo') { 51 | /* 52 | * ***************************************** 53 | * echo back to client 54 | * ***************************************** 55 | */ 56 | $this->echo($SocketID, $packet); 57 | return; 58 | } 59 | if ($packet->opcode === 'broadcast') { 60 | $this->broadCast($SocketID, $M); 61 | return; 62 | } 63 | /* 64 | * ***************************************** 65 | * unknown opcode-> do nothing 66 | * ***************************************** 67 | */ 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /server/resourceDefault.php: -------------------------------------------------------------------------------- 1 | } 18 | * This is just an example used here , you can send what ever you want. 19 | * ***************************************** 20 | */ 21 | 22 | $packet = $this->getPacket($M); 23 | if ($packet->opcode === 'jsonerror') { 24 | $this->Log("jsonerror closing #$SocketID"); 25 | $this->Close($SocketID); 26 | return; 27 | } 28 | 29 | $this->packet = $packet; 30 | if ($packet->opcode === 'quit') { 31 | /* 32 | * ***************************************** 33 | * client quits 34 | * ***************************************** 35 | */ 36 | $this->Log("QUIT; Connection closed to socket #$SocketID"); 37 | $this->Close($SocketID); 38 | return; 39 | } 40 | if ($packet->opcode === 'feedback') { 41 | /* 42 | * ***************************************** 43 | * send feedback to client with uuid found 44 | * in $packet 45 | * ***************************************** 46 | */ 47 | $this->feedback($packet); 48 | return; 49 | } 50 | if ($packet->opcode === 'echo') { 51 | /* 52 | * ***************************************** 53 | * send feedback to client with uuid found 54 | * in $packet 55 | * ***************************************** 56 | */ 57 | $this->echo($SocketID,$packet); 58 | return; 59 | } 60 | 61 | if ($packet->opcode === 'broadcast') { 62 | $this->broadCast($SocketID, $M); 63 | return; 64 | } 65 | /* 66 | * ***************************************** 67 | * unknown opcode-> do nothing 68 | * ***************************************** 69 | */ 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /server/runSocketServer.php: -------------------------------------------------------------------------------- 1 | default; 20 | /* 21 | * *********************************************** 22 | * create a logger 23 | * set directory for logfiles and 24 | * log to console 25 | * *********************************************** 26 | */ 27 | 28 | $logger = new logToFile($option['logfile'], 'phpwebsocketserver','',$option['console']); 29 | 30 | /* 31 | * ***************************************** 32 | * create server 33 | * ***************************************** 34 | */ 35 | $server = new webSocketServer($option['adress'], $logger, $option['certFile'], $option['pkFile']); 36 | /* 37 | * *********************************************** 38 | * set some server variables 39 | * *********************************************** 40 | */ 41 | $server->maxPerIP = 0; // 0=unlimited 42 | $server->maxClients = 0; // 0=unlimited 43 | $server->pingInterval=0; // unit is seconds; 0=no pings to clients 44 | /* 45 | * *********************************************** 46 | * instantiate backend 'applications' 47 | * *********************************************** 48 | */ 49 | $resDefault = new resourceDefault(); 50 | $resWeb = new resourceWeb(); 51 | $resPHP = new resourcePHP(); 52 | /* 53 | * *********************************************** 54 | * register backend 'applications' with server 55 | * *********************************************** 56 | */ 57 | $server->registerResource('/', $resDefault); 58 | $server->registerResource('/web', $resWeb); 59 | $server->registerResource('/php', $resPHP); 60 | /* 61 | * *********************************************** 62 | * now start it to have the server handle 63 | * requests from clients 64 | * *********************************************** 65 | */ 66 | 67 | $server->Start(); 68 | } 69 | 70 | } 71 | 72 | /* 73 | * *********************************************** 74 | * start 75 | * *********************************************** 76 | */ 77 | (new runSocketServer())->run(); 78 | 79 | -------------------------------------------------------------------------------- /phpClient/websocketPhp.php: -------------------------------------------------------------------------------- 1 | socketMaster, 1024); // wait for ACK 16 | $buff = $this->decodeFromServer($buff); 17 | $json = json_decode($buff); 18 | if ($json->opcode != 'ready') { 19 | $this->connected = false; 20 | return; 21 | } 22 | $this->fromUUID = $json->uuid; // assigned by server to this script 23 | $this->ident = $myIdent; // ident of other client 24 | } 25 | } 26 | 27 | final function broadcast($message) { 28 | $this->talk(['opcode' => 'broadcast', 'message' => $message]); 29 | } 30 | 31 | final function feedback($message, $otherIdent = '') { 32 | if ($this->uuid || $otherIdent != '') { // send to client identfied by UUDI or $otherIdent 33 | $this->talk([ 34 | 'opcode' => 'feedback', 35 | 'ident' => $otherIdent, // ident of another client 36 | 'uuid' => $this->uuid, // uuid of anotehr client 37 | 'message' => $message, 38 | 'fromUUID' => $this->fromUUID]); 39 | } 40 | } 41 | 42 | final function talk($msg) { 43 | if ($this->connected === false) { 44 | return; 45 | } 46 | $json = json_encode((object) $msg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); 47 | $len = mb_strlen($json); 48 | 49 | if ($len > $this->chunkSize && $this->chunkSize > 0) { 50 | 51 | $nChunks = floor($len / $this->chunkSize); 52 | if ($this->writeWait('bufferON')) { 53 | for ($i = 0, $j = 0; $i < $nChunks; $i++, $j += $this->chunkSize) { 54 | if ($this->writeWait(mb_substr($json, $j, $j + $this->chunkSize)) === false) { 55 | break; 56 | } 57 | } 58 | } 59 | if ($len % $this->chunkSize > 0) { 60 | $this->writeWait(mb_substr($json, $j, $j + $len % $this->chunkSize)); 61 | } 62 | $this->writeWait('bufferOFF'); 63 | } else { 64 | $this->writeWait($json); 65 | } 66 | } 67 | 68 | final function writeWait($m) { 69 | if ($this->connected === false) { 70 | return false; 71 | } 72 | $this->writeSocket($m); 73 | $buff = $this->readSocket(); // wait for ACK 74 | $ack = json_decode($buff); 75 | if ($ack->opcode != 'next') { 76 | $this->silent(); 77 | return false; 78 | } 79 | return true; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /autoload/classLoader.php: -------------------------------------------------------------------------------- 1 | isFile() || $file->getExtension() !== 'php') { 52 | continue; 53 | } 54 | 55 | $filename = basename($file->getFilename(), '.php'); 56 | $classMap[$filename] = str_replace('\\', '/', $file->getPathname()); 57 | } 58 | } 59 | 60 | file_put_contents($classMapFile, " 2 | 3 | # phpWebSocketServer 4 | 5 | ## NO DEPENDENCIES ## 6 | Server written in PHP to handle connections via websocksets **wss:// or ws://** 7 | and normal sockets over **ssl://** 8 | 9 | - 2025-13-07 refactored 10 | 11 | - 2021-09-30 fixed ping/pong cycle to clients. $pingInterval to start pinging clients 12 | 13 | - 2021-09-28 echo back to client implemented 14 | 15 | - 2021-09-25 php script nac now talk to other script 16 | 17 | 18 | - 2021-??-?? Runtime parameters are now read from file specified with option 19 | -i <inifile> ; default is websock.ini in server directory 20 | 21 | - 2021-08-22 Server and client can now handle very long messages (>8192B) 22 | no longer need to use own buffer mechanism. 23 | 24 | - 2020-12-07 it works also with PHP 8.0 25 | 26 | # A detailed documentation is located here: 27 | ## https://hgsweb.de/phpwebsocketDoc 28 | 29 | 30 | **NO DEPENDENCIES** 31 | 32 | implemented by 33 | - **Heinz Schweitzer** https://github.com/napengam/phpWebSocketServer 34 | to work for communicating over secure websocket wss:// 35 | and accept any other socket connection by PHP processes or other 36 | 37 | WebSocketServer is based on the implementation in PHP by 38 | - **Bryan Bliewert**, nVentis@GitHub https://github.com/nVentis/PHP-WebSocketServer 39 | 40 | The idea of *application classes' is taken from 41 | - **Simon Samtleben** https://github.com/bloatless/php-websocket 42 | 43 | See also https://tools.ietf.org/html/rfc6455 44 | 45 | 46 | # What is it good for ? 47 | 48 | This server allows you to establish communication between web applications living in a browser 49 | and enables backend scripts, in my case PHP, to communicate information back to web applications that 50 | have called the backend script to perform some action. 51 | 52 | Clients receive a UUID from the server upon connection. If the web application triggers 53 | backend scripts via AJAX it passes the UUID to the backend scripts. The script is now able to report 54 | back to the web client by sending the UUID along with an opcode 'feedback' and other parameters to the server. 55 | With the given UUID the server now knows to what web client to send the message. Loop closed ! 56 | 57 | See example in directory webClient 58 | 59 | # Installation 60 | 61 | - Transfer the director `phpWebSocketServer` to the documents root of your webserver 62 | 63 | # Configuration 64 | ## Part 1 65 | 66 | - Step into the `config` directory and adapt the 67 | - `websock_exmaple.ini` to your needs, rename or copy to websock.ini 68 | You will find some documentation in this file. 69 | 70 | ## Part 2 71 | 72 | To start and use the server see the [README](server/README.md) in directory server 73 | 74 | # Directories 75 | 76 | **include** 77 | 78 | Files included by server and clients 79 | 80 | **Server** 81 | 82 | Implemention of a server in php and php script to start server. 83 | Examples of classes to implement resources **/php** and **/web** 84 | 85 | **phpClient** 86 | 87 | Example of client written in PHP to connect and write to the server using resource **/php** 88 | 89 | **php2php** 90 | 91 | Example how to make one php script send messages to other php script 92 | via websocket 93 | 94 | **webClient** 95 | 96 | Example of web client to connect and communicate with the server using resource **/web** 97 | 98 | **websocketExtern** 99 | 100 | Conneting to external websocket servers to test client code 101 | 102 | 103 | 104 | # Some Numbers 105 | 106 | 107 | - Number of Files 108 | - 'php' => int 24 109 | - 'js' => int 2 110 | 111 | - Number of Lines of Code 112 | - 'php' => int 1950 113 | - 'js' => int 313 114 | 115 | - Size in KBytes 116 | - 'php' => float 59 117 | - 'js' => float 10 118 | 119 | -------------------------------------------------------------------------------- /server/logToFile.php: -------------------------------------------------------------------------------- 1 | logOnOff = true; 17 | $this->console = $console; 18 | $this->ident = $ident; 19 | $this->logFileOrg = $logDirFile; 20 | 21 | if ($logDir == '') { 22 | $logDir = getcwd(); 23 | } 24 | $this->logDir = $logDir; 25 | 26 | if (!is_dir($logDir)) { 27 | $this->error = "$logDir is not a directory"; 28 | } 29 | if (!is_writeable($logDir)) { 30 | $this->error = "$logDir is not writable"; 31 | } 32 | $this->pid = getmypid(); 33 | if ($this->error) { 34 | openlog($ident, LOG_PID, LOG_USER); 35 | syslog(LOG_ERR, "can not access LOGDIR $logDirFile; no loging"); 36 | closelog(); 37 | $this->logOnOff = false; 38 | } 39 | $this->logOpen($logDirFile); 40 | if ($message) { 41 | $this->log($message); 42 | } 43 | } 44 | 45 | function logOpen($logDirFile) { 46 | if ($this->logOnOff === false || $logDirFile == '') { 47 | return; 48 | } 49 | $num = 1; 50 | $fp = (object) pathinfo($logDirFile); 51 | 52 | if ($fp->extension) { 53 | $dot = "."; 54 | } else { 55 | $dot = ''; 56 | $fp->extension = ''; 57 | } 58 | $this->fh = fopen($logDirFile, 'a+'); 59 | 60 | if ($this->numLines($logDirFile) > $this->maxEntry) { 61 | $num = $this->logNum($fp->dirname . "/" . $fp->filename, $dot, $fp->extension); 62 | fclose($this->fh); 63 | rename($logDirFile, "$fp->dirname/$fp->filename$num$dot$fp->extension"); 64 | } 65 | $this->fh = fopen($logDirFile, 'a+'); 66 | $this->numLinesNow++; 67 | if ($this->fh === false) { 68 | openlog($this->ident, LOG_PID, LOG_USER); 69 | syslog(LOG_ERR, "can not open $logDirFile; no loging"); 70 | closelog(); 71 | $this->logOnOff = false; 72 | } 73 | } 74 | 75 | function log($m) { 76 | if ($this->console) { 77 | echo date('r') . ";" . $m . "\r\n"; 78 | } 79 | if ($this->logOnOff === false) { 80 | return; 81 | } 82 | if ($this->fh) { 83 | fputs($this->fh, date('r') . ";" . $m . "\r\n"); 84 | $this->numLinesNow++; 85 | if ($this->numLinesNow > $this->maxEntry) { 86 | $this->logClose(); 87 | $this->logOpen($this->logFileOrg); 88 | $this->numLinesNow = 0; 89 | } 90 | } 91 | } 92 | 93 | function logClose() { 94 | if ($this->fh) { 95 | fclose($this->fh); 96 | } 97 | } 98 | 99 | function logMode($onOff) { 100 | if ($this->error === '') { 101 | $this->logOnOff = $onOff; 102 | } 103 | } 104 | 105 | private function logNum($filename, $dot, $extension) { 106 | $max = 0; 107 | $out = []; 108 | foreach (glob("$filename*$dot$extension") as $fn) { 109 | preg_match_all('/[0-9]*/', $fn, $out); 110 | $n = trim(implode('', $out[0])); 111 | if ($n > $max) { 112 | $max = (int) $n; 113 | } 114 | } 115 | return $max + 1; 116 | } 117 | 118 | private function numLines($file) { 119 | $f = fopen($file, 'rb'); 120 | $lines = 0; 121 | while (!feof($f)) { 122 | $lines += substr_count(fread($f, 8192), "\n"); 123 | } 124 | fclose($f); 125 | $this->numLinesNow = $lines; 126 | return $lines; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /webClient/startWebClient.js: -------------------------------------------------------------------------------- 1 | /* global server, port */ 2 | window.addEventListener('load', startGUI, false); 3 | function startGUI() { 4 | 'use strict'; 5 | var sock, uuid, i, longString = ''; 6 | 7 | //******************************************** 8 | // Prepare the socket ecosystem :-) 9 | //******************************************* 10 | 11 | sock = socketWebClient(server, '/web'); 12 | sock.setCallbackReady(ready); 13 | sock.setCallbackReadMessage(readMessage); 14 | sock.setCallbackStatus(sockStatus); 15 | sock.setCallbackClose(closeSocket); 16 | 17 | sock.init(); 18 | 19 | 20 | //******************************************** 21 | // create a long message 22 | //******************************************* 23 | 24 | for (i = 0; i < 16 * 1024; i++) { 25 | longString += 'X'; 26 | } 27 | 28 | function sockStatus(m) { 29 | //******************************* 30 | // report connection status 31 | //******************************* 32 | document.getElementById('connect').innerHTML = m; 33 | } 34 | function closeSocket() { 35 | //******************************* 36 | // report connection status 37 | //******************************* 38 | document.getElementById('connect').innerHTML = 'Server is gone; closed socket'; 39 | } 40 | 41 | function readMessage(packet) { 42 | //******************************* 43 | // respond to messages from server 44 | //******************************* 45 | var obj; 46 | if (packet.opcode === 'broadcast') { 47 | obj = document.getElementById('broadcast'); 48 | obj.innerHTML += packet.message + '
'; 49 | } else if (packet.opcode === 'feedback') { 50 | obj = document.getElementById('feedback'); 51 | obj.innerHTML = packet.fromUUID + '---' + packet.message; 52 | } else if (packet.opcode === 'echo') { 53 | obj = document.getElementById('echomsg'); 54 | obj.innerHTML = packet.message; 55 | } 56 | } 57 | function ready() { 58 | // *********************************************** 59 | // we have now the uuid from the server and can start 60 | // *********************************************** 61 | uuid = sock.uuid(); 62 | document.getElementById('uuid').innerHTML = uuid; 63 | talkToOthers(); 64 | } 65 | 66 | function talkToOthers() { 67 | // *********************************************** 68 | // test if messages apear in other webclients in same order as send 69 | // no message is lost and very long message is buffered 70 | // **************************************************** 71 | sock.broadcast(`hallo11 from :${uuid}`); 72 | sock.broadcast(`hallo22 from :${uuid}`); 73 | sock.broadcast(`hallo33 from :${uuid}`); 74 | sock.broadcast(`hallo44 from :${uuid}`); 75 | sock.broadcast(longString + uuid); 76 | } 77 | 78 | 79 | function triggerAJAX() { 80 | //**************************************** 81 | // start dummy backend script 82 | //**************************************** 83 | var req; 84 | req = new XMLHttpRequest(); 85 | req.open("POST", '../phpClient/simulateBackend.php'); 86 | req.setRequestHeader("Content-Type", "application/json"); 87 | req.send(JSON.stringify({'uuid': uuid})); 88 | } 89 | function echo() { 90 | sock.echo(`ECHO from :${uuid}`); 91 | } 92 | //******************************************** 93 | // instrument the buttons 94 | //******************************************* 95 | 96 | document.getElementById('open').onclick = sock.init; 97 | document.getElementById('close').onclick = sock.quit; 98 | document.getElementById('ready').onclick = talkToOthers; 99 | document.getElementById('ajax').onclick = triggerAJAX; 100 | document.getElementById('echo').onclick = echo; 101 | document.getElementById('uuid').innerHTML = uuid; 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | To get an idea how things work and relate to each other 4 | on the server side,have a look into `runSocketServer.php`. 5 | 6 | Clients to test the server are located in 7 | 8 | - `../webClient` 9 | - `../phpClient` 10 | 11 | # Files 12 | 13 | ## RFC6455.php 14 | 15 | A php trait used in class `webSocketServer.php` 16 | Implements methods 17 | 18 | Method|What 19 | ------|---- 20 | decode| decode messages coming from a websocket 21 | encode| encode message to be send to a websocket 22 | handshake|handle handshake with connecting clients 23 | 24 | ## getOptions.php 25 | 26 | Reads default websock.ini for essential options 27 | 28 | ## websock.ini 29 | 30 | ; holds these options 31 | ; 32 | adress='ws://localhost:8096' 33 | logfile=D:/temp/websock.log 34 | console=false 35 | certFile='' 36 | pkFile='' 37 | 38 | ## logToFile 39 | 40 | Class to handle all loging and log rotation. 41 | 42 | ## resource.php 43 | 44 | Base class that implements empty methodes required in order 45 | to register a resource with the server. 46 | 47 | Method|What 48 | ------|---- 49 | onData | data received from client 50 | onClose | socket has been closed AND deleted 51 | onError | any connection-releated error 52 | onOther | any connection-releated notification 53 | 54 | 55 | If any of the above methods are missing the application will be rejected by the server. 56 | 57 | Method|What 58 | ------|---- 59 | getPacket | decode JSON packet and check for JSON errors 60 | 61 | This class also provides a method enabling the server to register itself with 62 | the application, thus giving access to information within the server like sockets 63 | and client structures. 64 | 65 | Method|What 66 | ------|---- 67 | registerServer | // as said 68 | 69 | 70 | 71 | 72 | ## resourceDefault.php 73 | 74 | This class extends `resource.php` 75 | Class that will serve requests for resource **/** 76 | 77 | `[ws,wss,tcp,ssl]://socket.server.php:port/` 78 | 79 | ## resourcePHP.php 80 | This class extends `resource.php` 81 | Class that will serve requests for resource **PHP** 82 | 83 | `[tcp,ssl]://socket.server.php:port/php` 84 | 85 | 86 | ## resourceWeb.php 87 | This class extends `resource.php` 88 | Class that will serve requests for resource **web** 89 | 90 | `[ws,wss]://socket.server.php:port/web` 91 | 92 | ## webSocketServer.php 93 | 94 | Class to implement the server. 95 | Consumes trait `RFC6455.php` 96 | 97 | The server handles connection request from clients, performes a handshake with clients. 98 | 99 | To keep track of all the clients an associated sockets there are two array where we store 100 | this information. 101 | `$this->Sockets=[]` 102 | `$this->Clients=[]` 103 | 104 | Lets say a client is accepted on `$clientSocket` then 105 | 106 | With `SocketID=intval($clientSocket)` we generate an index into 107 | `$this->Sockets[$SocketID]=$clientSocket;` 108 | `$this->Clients[$SocketID]=(objcet)[/*attributes*/]` 109 | under witch we store and retreive the needed information. 110 | 111 | 112 | Upon a successful handshake, the client is registered with the server and incoming messages 113 | will be routed to the resource, application, the client specified in the **GET** request. 114 | In the given examples resources are **/web** and **/php** 115 | 116 | Next the server sends a message **ready** to the client, that is waiting for this 117 | message and a UUID which is tracked along other informations for this client. 118 | 119 | Any incoming message is allways acknowledged with a **next** message to the client. 120 | Clients should wait for this message to arrive, before sending another message. 121 | 122 | Based on the pattern `bufferON` and `bufferOFF`, found as a string at the start of 123 | a message from a client, the server will turn *on* or *off*, buffering of very long 124 | messages (above 8K) for the given client. 125 | 126 | In the given examples, messages routed to the requested resource are expected to be in JSON format 127 | `{'opcode': opcode, 'message':messsage .....}` 128 | 129 | Feel free to use whatever supports your needs. 130 | 131 | 132 | ## runSocketServer.php 133 | 134 | First create an instance of the server 135 | 136 | 137 | Next registers the application 138 | - `resourceDefault.php` 139 | - `resourceWeb.php` 140 | -`resourcePHP.php` 141 | 142 | with the server. 143 | 144 | Next starts the server. 145 | 146 | On a shell on Linux just start it, with logging to console, like: 147 | 148 | > php runSocketserver.php -console 149 | 150 | you shoud then see an out put like the one below on system using SSL 151 | 152 | 153 | > php runSocketServer.php -console 154 | > Wed, 14 Jul 2021 10:01:21 +0200; Server initialized on Linux xxx.yyy.net:8096 ssl:// 155 | > Wed, 14 Jul 2021 10:01:21 +0200; Starting server... 156 | > Wed, 14 Jul 2021 10:01:21 +0200; Registered resource : / 157 | > Wed, 14 Jul 2021 10:01:21 +0200; Registered resource : /web 158 | 159 | on a system not using SSL you should see a similar output like the one below 160 | 161 | > php runSocketServer.php -console 162 | > Wed, 14 Jul 2021 09:04:35 +0200; Server initialized on WINNT localhost:8091 163 | > Wed, 14 Jul 2021 09:04:35 +0200; Starting server... 164 | > Wed, 14 Jul 2021 09:04:35 +0200; Registered resource : / 165 | > Wed, 14 Jul 2021 09:04:35 +0200; Registered resource : /web 166 | > Wed, 14 Jul 2021 09:04:35 +0200; Registered resource : /php 167 | > 168 | 169 | 170 | 171 | ## websocketserver.service 172 | 173 | Unit file to create service/deamon for websocketserver on Linux 174 | 175 | -------------------------------------------------------------------------------- /webClient/socketWebClient.js: -------------------------------------------------------------------------------- 1 | function socketWebClient(server, app) { 2 | 'use strict'; 3 | 4 | let queue = []; 5 | let uuidValue; 6 | let socket = null; 7 | const chunkSize = 0 * 1024; // Define chunk size, currently 0 8 | let socketOpen = false; 9 | let socketSend = false; 10 | 11 | function uuid() { 12 | return uuidValue; 13 | } 14 | 15 | function init() { 16 | if (socket !== null) { 17 | socket.close(); 18 | } 19 | 20 | //******************************************** 21 | // connect to server at port 22 | //******************************************** 23 | try { 24 | socket = new WebSocket(server + app); 25 | callbackStatus('Try to connect ...'); 26 | } catch (e) { 27 | socket = null; 28 | return; 29 | } 30 | 31 | socket.onopen = function () { 32 | queue = []; 33 | callbackStatus('Connected'); 34 | }; 35 | 36 | socket.onerror = function () { 37 | if (!socketSend) { 38 | callbackStatus('Cannot connect to specified server'); 39 | } 40 | socketSend = false; 41 | socketOpen = false; 42 | queue = []; 43 | }; 44 | 45 | //******************************************** 46 | // handle message from server 47 | //******************************************** 48 | socket.onmessage = function (msg) { 49 | if (msg.data.length === 0 || msg.data.includes('pong')) { 50 | return; 51 | } 52 | 53 | const packet = JSON.parse(msg.data); 54 | 55 | switch (packet.opcode) { 56 | case 'next': 57 | // Server is ready for the next message 58 | queue.shift(); 59 | if (queue.length > 0) { 60 | const nextMsg = queue[0]; 61 | socket.send(nextMsg); 62 | } else { 63 | queue = []; 64 | } 65 | break; 66 | 67 | case 'ready': 68 | // Server is ready; receive UUID from server 69 | socketOpen = true; 70 | socketSend = true; 71 | uuidValue = packet.uuid; 72 | callbackReady(packet); 73 | break; 74 | 75 | case 'close': 76 | // Server has closed the connection 77 | socketOpen = false; 78 | socketSend = false; 79 | callbackStatus('Server closed connection'); 80 | break; 81 | 82 | default: 83 | // Unknown opcode; pass message to external function for handling 84 | callbackReadMessage(packet); 85 | break; 86 | } 87 | 88 | 89 | }; 90 | 91 | //******************************************** 92 | // handle socket close 93 | //******************************************** 94 | socket.onclose = function () { 95 | queue = []; 96 | socketOpen = false; 97 | socketSend = false; 98 | callbackClose(); 99 | }; 100 | } 101 | 102 | //******************************************** 103 | // queue messages to be sent 104 | //******************************************** 105 | function sendMsg(msgObj) { 106 | if (!socketSend || !socketOpen) { 107 | return; 108 | } 109 | 110 | const msg = JSON.stringify(msgObj); 111 | let sendNow = false; 112 | 113 | if (msg.length < chunkSize || chunkSize === 0) { 114 | queue.push(msg); 115 | } else { 116 | if (queue.length === 0) { 117 | sendNow = true; 118 | } 119 | queue.push('bufferON'); // Start of large message chunks 120 | 121 | const nChunks = Math.floor(msg.length / chunkSize); 122 | for (let i = 0, j = 0; i < nChunks; i++, j += chunkSize) { 123 | queue.push(msg.slice(j, j + chunkSize)); 124 | } 125 | 126 | if (msg.length % chunkSize > 0) { 127 | queue.push(msg.slice(nChunks * chunkSize)); 128 | } 129 | queue.push('bufferOFF'); // End of large message chunks 130 | } 131 | 132 | if ((queue.length === 1 || sendNow) && socketOpen) { 133 | socket.send(queue[0]); 134 | sendNow = false; 135 | } 136 | } 137 | 138 | //******************************************** 139 | // Dummy functions; should be set from outside 140 | //******************************************** 141 | let callbackStatus = function (p) { 142 | return p; 143 | }; 144 | let callbackReady = function (p) { 145 | return p; 146 | }; 147 | let callbackReadMessage = function (p) { 148 | return p; 149 | }; 150 | let callbackClose = function () { 151 | return ''; 152 | }; 153 | 154 | //************************************************** 155 | // Functions to set/overwrite dummy functions above 156 | //************************************************** 157 | function setCallbackStatus(func) { 158 | callbackStatus = func; 159 | } 160 | function setCallbackReady(func) { 161 | callbackReady = func; 162 | } 163 | function setCallbackReadMessage(func) { 164 | callbackReadMessage = func; 165 | } 166 | function setCallbackClose(func) { 167 | callbackClose = func; 168 | } 169 | 170 | //******************************************** 171 | // Convenience functions for message types 172 | //******************************************** 173 | function broadcast(msg) { 174 | sendMsg({'opcode': 'broadcast', 'message': msg}); 175 | } 176 | 177 | function feedback(msg, toUUID) { 178 | sendMsg({'opcode': 'feedback', 'message': msg, 'uuid': toUUID, 'from': uuid}); 179 | } 180 | 181 | function echo(msg) { 182 | sendMsg({'opcode': 'echo', 'message': msg}); 183 | } 184 | 185 | function quit() { 186 | socket.close(); 187 | socketOpen = false; 188 | socketSend = false; 189 | } 190 | 191 | function isOpen() { 192 | return socketOpen; 193 | } 194 | 195 | //******************************************** 196 | // Expose functions to the caller 197 | //******************************************** 198 | return { 199 | init, 200 | sendMsg, 201 | uuid, 202 | quit, 203 | isOpen, 204 | setCallbackStatus, 205 | setCallbackReady, 206 | setCallbackReadMessage, 207 | setCallbackClose, 208 | broadcast, 209 | feedback, 210 | echo 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /server/RFC_6455.php: -------------------------------------------------------------------------------- 1 | opcode === 10) ? 138 : (($this->opcode === 9) ? 137 : 129); 11 | 12 | // Reset opcode to 1 for continuation frames 13 | $this->opcode = 1; 14 | 15 | // Determine the payload length and construct the header accordingly 16 | if ($length <= 125) { 17 | $header[] = $length; 18 | } elseif ($length <= 65535) { 19 | $header[] = 126; 20 | $header[] = ($length >> 8) & 0xFF; 21 | $header[] = $length & 0xFF; 22 | } else { 23 | $header[] = 127; 24 | for ($i = 7; $i >= 0; $i--) { 25 | $header[] = ($length >> ($i * 8)) & 0xFF; 26 | } 27 | } 28 | 29 | // Create the final header as a string and return it concatenated with the message 30 | return implode(array_map("chr", $header)) . $message; 31 | } 32 | 33 | public function readDecode($socketID) { 34 | $socket = $this->Sockets[$socketID]; 35 | $frame = fread($socket, 8192); 36 | 37 | if (empty($frame)) { 38 | $this->opcode = 8; // Close frame if empty 39 | return; 40 | } 41 | 42 | $this->fin = (ord($frame[0]) & 128) !== 0; 43 | $this->opcode = ord($frame[0]) & 15; 44 | $length = ord($frame[1]) & 127; 45 | 46 | if ($length === 0) { 47 | $this->opcode = 8; 48 | return; 49 | } 50 | 51 | $masks = ''; 52 | $dataOffset = 2; 53 | 54 | if ($length === 126) { 55 | $length = unpack('n', substr($frame, 2, 2))[1]; 56 | $dataOffset = 4; 57 | } elseif ($length === 127) { 58 | $length = unpack('J', substr($frame, 2, 8))[1]; 59 | $dataOffset = 10; 60 | } 61 | 62 | $masks = substr($frame, $dataOffset, 4); 63 | $data = substr($frame, $dataOffset + 4, $length); 64 | 65 | // Read additional chunks if necessary to complete the payload 66 | $remaining = $length - strlen($data); 67 | while ($remaining > 0) { 68 | $chunk = fread($socket, min(8192, $remaining)); 69 | $data .= $chunk; 70 | $remaining -= strlen($chunk); 71 | } 72 | 73 | // Apply masking 74 | $text = ''; 75 | for ($i = 0; $i < $length; $i++) { 76 | $text .= $data[$i] ^ $masks[$i % 4]; 77 | } 78 | 79 | return $text; 80 | } 81 | 82 | protected function Handshake($Socket, $Buffer) { 83 | $SocketID = (int) $Socket; 84 | $Headers = []; 85 | $errorResponses = []; 86 | $lines = explode("\n", $Buffer); 87 | 88 | // Parse headers and extract requested resource 89 | foreach ($lines as $line) { 90 | if (strpos($line, ":") !== false) { 91 | [$key, $value] = explode(":", $line, 2); 92 | $Headers[strtolower(trim($key))] = trim($value); 93 | } elseif (stripos($line, "get ") === 0) { 94 | if (preg_match("/GET (.*) HTTP/i", $line, $reqResource)) { 95 | $Headers['get'] = trim($reqResource[1]); 96 | } 97 | } 98 | } 99 | 100 | $this->Log("Handshake: " . ($Headers['get'] ?? 'Unknown') . " Client"); 101 | 102 | // Check required headers 103 | $requiredHeaders = ['host', 'origin', 'sec-websocket-key', 'upgrade', 'connection', 'sec-websocket-version']; 104 | foreach ($requiredHeaders as $key) { 105 | if (!isset($Headers[$key])) { 106 | $this->sendErrorResponse($Socket, $SocketID, "HTTP/1.1 400 Bad Request", "Missing header: $key"); 107 | return false; 108 | } 109 | } 110 | 111 | // Validate WebSocket upgrade headers 112 | if (strtolower($Headers['upgrade']) !== 'websocket' || 113 | stripos($Headers['connection'], 'upgrade') === false) { 114 | $errorResponses[] = "HTTP/1.1 400 Bad Request"; 115 | } 116 | 117 | // Validate WebSocket version 118 | if ($Headers['sec-websocket-version'] !== '13') { 119 | $errorResponses[] = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13"; 120 | } 121 | 122 | // Validate HTTP method 123 | if (empty($Headers['get'])) { 124 | $errorResponses[] = "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; 125 | } 126 | 127 | // Send accumulated error responses if any 128 | if (!empty($errorResponses)) { 129 | $this->sendErrorResponse($Socket, $SocketID, implode("\r\n", $errorResponses), "Invalid handshake request"); 130 | return false; 131 | } 132 | 133 | // Complete the WebSocket handshake 134 | $acceptToken = base64_encode(pack('H*', sha1($Headers['sec-websocket-key'] . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); 135 | $statusLine = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $acceptToken\r\n\r\n"; 136 | fwrite($Socket, $statusLine); 137 | 138 | // Set client type and metadata if available 139 | $clientType = (strcasecmp($Headers['client-type'] ?? '', 'php') === 0) ? 'php' : 'websocket'; 140 | $client = $this->Clients[$SocketID]; 141 | $client->clientType = $clientType; 142 | $client->ident = $Headers['ident'] ?? null; 143 | $client->allowRemote = $Headers['allowRemote'] ?? null; 144 | $client->Handshake = true; 145 | 146 | // Log and associate the app if configured 147 | $this->Log('ClientType: ' . $clientType); 148 | if (isset($this->allApps[$Headers['get']])) { 149 | $client->app = $this->allApps[$Headers['get']]; 150 | } 151 | 152 | return true; 153 | } 154 | 155 | // Helper method for sending error responses 156 | private function sendErrorResponse($Socket, $SocketID, $message, $logMessage) { 157 | fwrite($Socket, $message); 158 | $this->onError($SocketID, "Handshake aborted - $logMessage"); 159 | $this->Close($Socket); 160 | } 161 | 162 | function extractIPort($inIP) { 163 | // Trim spaces and match IPv4 or IPv6 addresses with optional port 164 | // [2001:db8:85a3:8d3:1319:8a2e:370:7348]:8765 ????? 165 | // 2001:db8:85a3:8d3:1319:8a2e:370:7348 166 | // 127.0.0.1:1234 167 | 168 | 169 | $inIP = preg_replace('/\s+/', '', $inIP); 170 | 171 | // Match patterns for IPv6 with port, IPv6 without port, IPv4 with port, and IPv4 without port 172 | if (preg_match('/^\[([^\]]+)\](?::(\d+))?$/', $inIP, $matches) || // IPv6 with optional port 173 | preg_match('/^([0-9.]+)(?::(\d+))?$/', $inIP, $matches)) { // IPv4 with optional port 174 | return (object) ['ip' => $matches[1], 'port' => $matches[2] ?? '']; 175 | } 176 | 177 | // Return as-is if no pattern matches (fallback for invalid input) 178 | return (object) ['ip' => $inIP, 'port' => '']; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /phpClient/websocketCore.php: -------------------------------------------------------------------------------- 1 | ident = $ident; 11 | $context = stream_context_create(); 12 | 13 | // Extract protocol and set default port 14 | $parts = explode('://', $Address, 2); 15 | $protocol = (count($parts) > 1) ? strtolower($parts[0]) : 'tcp'; 16 | $Address = (count($parts) > 1) ? $parts[1] : $Address; 17 | 18 | $isSecure = ($protocol === 'ssl' || $protocol === 'wss'); 19 | $defaultPort = $isSecure ? '443' : '80'; 20 | $prot = $isSecure ? 'ssl://' : 'tcp://'; 21 | 22 | if ($isSecure) { 23 | stream_context_set_option($context, 'ssl', 'allow_self_signed', true); 24 | stream_context_set_option($context, 'ssl', 'verify_peer', false); 25 | stream_context_set_option($context, 'ssl', 'verify_peer_name', false); 26 | } 27 | 28 | // Extract endpoint and default to '/' 29 | [$host, $app] = explode('/', $Address, 2) + [null, '/']; 30 | $app = '/' . $app; 31 | 32 | // Extract port if specified 33 | [$host, $port] = explode(':', $host, 2) + [null, $defaultPort]; 34 | $addressWithPort = "$prot$host:$port"; 35 | 36 | $errno = 0; 37 | $errstr = ''; 38 | $this->socketMaster = stream_socket_client($addressWithPort, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); 39 | 40 | if (!$this->socketMaster) { 41 | $this->connected = false; 42 | return false; 43 | } 44 | 45 | $this->connected = true; 46 | fwrite($this->socketMaster, $this->setHandshake($host, $app)); 47 | $buffer = fread($this->socketMaster, 1024); 48 | 49 | if (!$this->getHandshake($buffer)) { 50 | $this->silent(); 51 | echo $this->errorHandshake; 52 | return false; 53 | } 54 | 55 | // Set a timeout for non-blocking client actions 56 | stream_set_timeout($this->socketMaster, $this->timeout); 57 | return true; 58 | } 59 | 60 | final function writeSocket($message) { 61 | if ($this->connected) { 62 | fwrite($this->socketMaster, $this->encodeForServer($message)); 63 | } 64 | } 65 | 66 | final function readSocket() { 67 | 68 | if ($this->connected === false) { 69 | return ''; 70 | } 71 | $buff = []; 72 | $i = 0; 73 | do { // probaly reading fragements 74 | $continue = false; 75 | $buff[$i] = $this->decodeFromServer(fread($this->socketMaster, 8192)); 76 | if (stream_get_meta_data($this->socketMaster)['timed_out']) { 77 | $this->connected = false; 78 | return ''; 79 | } 80 | switch ($this->opcode) { 81 | case 9: // Ping frame 82 | $this->opcode = 10; // Respond with pong 83 | $m = implode('', $buff); 84 | $this->writeSocket($m, strlen($m)); 85 | $this->fin = false; // Continue reading 86 | $continue = true; 87 | break; 88 | 89 | case 10: // Pong frame 90 | $this->fin = false; // Ignore, continue reading 91 | $continue = true; 92 | break; 93 | case 8: // Close frame 94 | $this->silent(); // Close connection 95 | return ''; 96 | 97 | default: 98 | // Adjust length remaining to read 99 | $this->length -= strlen($buff[$i]); 100 | break; 101 | } 102 | if ($continue) { 103 | $continue = false; 104 | continue; 105 | } 106 | $i++; 107 | while ($this->length > 0) { // data buffered by socket 108 | $buff[$i] = fread($this->socketMaster, 8192); 109 | if (stream_get_meta_data($this->socketMaster)['timed_out']) { 110 | $this->connected = false; 111 | return ''; 112 | } 113 | $this->length -= strlen($buff[$i]); 114 | $i++; 115 | } 116 | } while ($this->fin == false); 117 | return implode('', $buff); 118 | } 119 | 120 | final function silent() { 121 | if ($this->connected) { 122 | $this->writeSocket(''); // close 123 | fclose($this->socketMaster); 124 | $this->connected = false; 125 | } 126 | } 127 | 128 | private function setHandshake($server, $app = '/') { 129 | $this->key = random_bytes(16); 130 | $key = base64_encode($this->key); 131 | 132 | // Expected token calculated from key and the WebSocket GUID 133 | $sah1 = sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 134 | $this->expectedToken = base64_encode(hex2bin($sah1)); 135 | 136 | // Determine protocol based on $this->prot 137 | $prot = ($this->prot === 'ssl://') ? "https://" : "http://"; 138 | 139 | // Assemble handshake request headers 140 | $req = [ 141 | "GET $app HTTP/1.1", 142 | "Host: $server", 143 | "Upgrade: websocket", 144 | "Connection: Upgrade", 145 | "Sec-WebSocket-Key: $key", 146 | "Origin: {$prot}{$server}", 147 | "Sec-WebSocket-Version: 13", 148 | "Client-Type: php", // Private, not part of RFC6455 149 | "Ident: $this->ident", // Private, not part of RFC6455 150 | "allowRemote: ''" // Private, not part of RFC6455 151 | ]; 152 | 153 | return implode("\r\n", $req) . "\r\n\r\n"; 154 | } 155 | 156 | private function getHandshake($Buffer) { 157 | $Headers = []; 158 | $this->errorHandshake = $Buffer; 159 | $Lines = explode("\n", $Buffer); 160 | foreach ($Lines as $Line) { 161 | if (strpos($Line, ":") !== false) { 162 | $Header = explode(":", $Line, 2); 163 | $Headers[strtolower(trim($Header[0]))] = trim($Header[1]); 164 | } else if (stripos($Line, "HTTP/") !== false) { 165 | $Headers['101'] = trim($Line); 166 | } 167 | } 168 | foreach (['101', 'upgrade', 'connection', 'sec-websocket-accept']as $key) { 169 | if (isset($Headers[$key]) === false) { 170 | return false; 171 | } 172 | } 173 | 174 | if (stripos($Headers['101'], "HTTP/1.1 101") === false) { 175 | return false; 176 | } 177 | if (strcasecmp($Headers['upgrade'], 'websocket') <> 0) { 178 | return false; 179 | } 180 | if (strcasecmp($Headers['connection'], 'Upgrade') <> 0) { 181 | return false; 182 | } 183 | if ($Headers['sec-websocket-accept'] != $this->expectedToken) { 184 | return false; 185 | } 186 | $this->errorHandshake = ''; 187 | return true; 188 | } 189 | 190 | final function encodeForServer($M) { 191 | $L = strlen($M); 192 | $bHead = []; 193 | 194 | // Set the first byte based on the opcode and fragment 195 | if ($L === 0) { 196 | $bHead[] = 136; // Close frame if message length = 0 197 | } else { 198 | $bHead[] = $this->finBit ? ($this->firstFragment ? ($this->opcode === 10 ? 138 : 129) : 128) : ($this->firstFragment ? 1 : 0); 199 | 200 | $this->firstFragment = !$this->finBit; 201 | } 202 | 203 | // Prepare the payload length and mask bit 204 | if ($L <= 125) { 205 | $bHead[] = $L | 128; 206 | } elseif ($L <= 65535) { 207 | $bHead = array_merge($bHead, [126 | 128, ($L >> 8) & 255, $L & 255]); 208 | } else { 209 | $bHead = array_merge($bHead, [127 | 128, ($L >> 56) & 255, ($L >> 48) & 255, ($L >> 40) & 255, ($L >> 32) & 255, ($L >> 24) & 255, ($L >> 16) & 255, ($L >> 8) & 255, $L & 255]); 210 | } 211 | 212 | // Generate masking key and apply it to the message payload 213 | $masks = random_bytes(4); 214 | $maskedPayload = ''; 215 | 216 | for ($i = 0; $i < $L; $i++) { 217 | $maskedPayload .= $M[$i] ^ $masks[$i % 4]; 218 | } 219 | 220 | // Combine header, masking key, and masked payload 221 | return implode(array_map("chr", $bHead)) . $masks . $maskedPayload; 222 | } 223 | 224 | final function decodeFromServer($frame) { 225 | if ($frame === false || $frame == '' || $frame == null || !is_string($frame)) { 226 | $this->opcode = 8; // force close connetion 227 | $this->fin = true; 228 | $this->length = 0; 229 | $this->frame = ''; 230 | return ''; 231 | } 232 | 233 | // Detects and processes WebSocket frames, including ping, pong, and fragmented frames. 234 | $this->fin = (ord($frame[0]) & 0b10000000) !== 0; // FIN bit 235 | $this->opcode = ord($frame[0]) & 0b00001111; // Opcode 236 | $this->frame = $frame; 237 | 238 | $length = ord($frame[1]) & 0b01111111; // Mask length byte to get payload length 239 | $poff = 2; // Default payload offset for lengths <= 125 240 | 241 | if ($length === 126) { 242 | $length = (ord($frame[2]) << 8) | ord($frame[3]); 243 | $poff = 4; 244 | } elseif ($length === 127) { 245 | // Assemble 64-bit length for extended payloads 246 | $length = 0; 247 | for ($i = 2; $i < 10; $i++) { 248 | $length = ($length << 8) | ord($frame[$i]); 249 | } 250 | $poff = 10; 251 | } 252 | 253 | $this->length = $length; 254 | return substr($frame, $poff, $length); // Extract payload data starting at offset 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /server/webSocketServer.php: -------------------------------------------------------------------------------- 1 | logging = $logger; 38 | $this->token = bin2hex(random_bytes(8)); 39 | 40 | /* 41 | * *********************************************** 42 | * as of 2021-07-21 context is set with 43 | * cert.pem and privkey.pem 44 | * *********************************************** 45 | */ 46 | $usingSSL = ''; 47 | $context = stream_context_create(); 48 | $Port = ''; 49 | if ($this->isSecure($Address, $Port)) { // $Port will set by function 50 | stream_context_set_option($context, 'ssl', 'local_cert', $certFile); 51 | stream_context_set_option($context, 'ssl', 'local_pk', $pkFile); 52 | stream_context_set_option($context, 'ssl', 'verify_peer', false); 53 | $usingSSL = "ssl://"; 54 | } 55 | $socket = stream_socket_server("$usingSSL$Address:$Port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); 56 | 57 | $this->Log("Server initialized on " . PHP_OS . " $Address:$Port $usingSSL"); 58 | if (!$socket) { 59 | $this->Log("Error $errno creating stream: $errstr", true); 60 | openlog('websock', LOG_PID, LOG_USER); 61 | syslog(LOG_ERR, "Error $errno creating stream: $errstr with $usingSSL$Address:$Port"); 62 | closelog(); 63 | exit; 64 | } 65 | 66 | $this->Sockets[intval($socket)] = $socket; 67 | $this->socketMaster = $socket; 68 | $this->allowedIP[] = gethostbyname($Address); 69 | $this->allowedIP[] = '::1'; 70 | 71 | error_reporting($this->errorReport); 72 | set_time_limit($this->timeLimit); 73 | if ($this->implicitFlush) { 74 | ob_implicit_flush(); 75 | } 76 | } 77 | 78 | private function isSecure(&$Address, &$port) { 79 | $secure = false; 80 | $arr = explode('://', $Address); 81 | if (count($arr) > 1) { 82 | if (strncasecmp($arr[0], 'ssl', 3) == 0 || strncasecmp($arr[0], 'wss', 3) == 0) { 83 | $Address = $arr[1]; 84 | $secure = true; 85 | $port = '443'; // default 86 | } else { 87 | $Address = $arr[1]; // just the host 88 | $port = '80'; // default 89 | } 90 | } 91 | /* 92 | * *********************************************** 93 | * extract port from $Address if given 94 | * *********************************************** 95 | */ 96 | $arr = explode(':', $Address); 97 | if (count($arr) > 1) { 98 | $Address = $arr[0]; 99 | $port = $arr[1]; // overwrite default 100 | } 101 | return $secure; 102 | } 103 | 104 | public function Start() { 105 | 106 | $this->Log("Starting server..."); 107 | foreach ($this->allApps as $appName => $class) { 108 | $this->Log("Registered resource : $appName"); 109 | } 110 | $a = true; 111 | $socketArrayWrite = $socketArrayExceptions = NULL; 112 | $startTime = time(); 113 | while ($a) { 114 | $socketArrayRead = $this->Sockets; 115 | $ncon = stream_select($socketArrayRead, $socketArrayWrite, $socketArrayExceptions, 1, 000); 116 | if ($ncon === 0) { 117 | /* 118 | * *********************************************** 119 | * no news after one second; we can do other tasks. 120 | * Here we continue to wait for another second 121 | * *********************************************** 122 | */ 123 | 124 | if ($this->pingInterval > 0 && time() - $startTime > $this->pingInterval) { 125 | if ($this->pingClients()) { 126 | $this->Log("Ping Clients"); 127 | } 128 | $startTime = time(); 129 | } 130 | 131 | continue; 132 | } 133 | foreach ($socketArrayRead as $Socket) { 134 | $SocketID = intval($Socket); 135 | if ($Socket === $this->socketMaster) { 136 | /* 137 | * *********************************************** 138 | * new client 139 | * *********************************************** 140 | */ 141 | $clientSocket = stream_socket_accept($Socket); 142 | if (!is_resource($clientSocket)) { 143 | $this->Log("$SocketID, Connection could not be established"); 144 | continue; 145 | } 146 | /* 147 | * *********************************************** 148 | * get IP:Port of client 149 | * *********************************************** 150 | */ 151 | $ipport = stream_socket_get_name($clientSocket, true); 152 | $ip = $this->extractIPort($ipport); // can be ipv4 or ipv6 153 | $this->Log("Connecting from IP: $ip->ip"); 154 | $SocketID = intval($clientSocket); 155 | $this->Clients[$SocketID] = (object) [ 156 | 'ID' => $SocketID, 157 | 'uuid' => '', 158 | 'clientType' => null, // not part of RFC6455 159 | 'Handshake' => false, 160 | 'timeCreated' => time(), // not used yet 161 | 'bufferON' => false, 162 | 'fin' => true, // RFC6455 final fragment in message 163 | 'buffer' => [], // buffers message chunks 164 | 'app' => NULL, 165 | 'ip' => $ip->ip, 166 | 'fyi' => '', 167 | 'ident' => '', // id set from client not part of RFC6455 168 | 'allowremote' => 'no', // id set from client not part of RFC6455 169 | 'expectPong' => false // is true if ping has been send 170 | ]; 171 | $this->Sockets[$SocketID] = $clientSocket; 172 | $this->Log("New client connecting from $ipport on socket #$SocketID\r\n"); 173 | continue; // done so far for this new client 174 | } 175 | 176 | /* 177 | * *********************************************** 178 | * setting unbuffered read, could be dangerous 179 | * because a client can send unlimited amount of 180 | * data and block the server. Therefor I do not 181 | * use this option. Client should send long messages 182 | * in chunks. 183 | * *********************************************** 184 | */ 185 | 186 | //stream_set_read_buffer($Socket, 0); // no buffering hgs 01.05.2021 187 | 188 | 189 | $Client = $this->Clients[$SocketID]; 190 | 191 | if ($Client->Handshake) { 192 | /* 193 | * *********************************************** 194 | * Handshake and checks have passsed. 195 | * get message from client 196 | * *********************************************** 197 | */ 198 | 199 | $message = $this->extractMessage($SocketID); 200 | if ($message != '') { 201 | /* 202 | * *********************************************** 203 | * route message to application class 204 | * *********************************************** 205 | */ 206 | $Client->app->onData($SocketID, $message); 207 | } 208 | continue; 209 | } 210 | /* 211 | * *********************************************** 212 | * read data for handshake from socket and check 213 | * *********************************************** 214 | */ 215 | 216 | $dataBuffer = fread($Socket, $this->bufferLength); 217 | if ($dataBuffer === false || 218 | strlen($dataBuffer) == 0 || 219 | strlen($dataBuffer) >= $this->bufferChunk) { // to avoid malicious overload 220 | $this->onError($SocketID, "Client disconnected by Server - TCP connection lost"); 221 | $this->Close($Socket); 222 | continue; 223 | } 224 | /* 225 | * *********************************************** 226 | * handshake 227 | * *********************************************** 228 | */ 229 | 230 | if ($this->Handshake($Socket, $dataBuffer) === false) { 231 | continue; // something is wrong 232 | } 233 | /* 234 | * *********************************************** 235 | * handshake according RFC 6455 is ok . 236 | * Now,for this client, check for apps and connections 237 | * *********************************************** 238 | */ 239 | if ($this->specificChecks($SocketID) === false) { 240 | continue; // something is wrong 241 | } 242 | /* 243 | * *********************************************** 244 | * all checks passed now let client work 245 | * *********************************************** 246 | */ 247 | $this->Log("Telling Client to start on #$SocketID"); 248 | $uuid = $this->guidv4(); 249 | $msg = (object) ['opcode' => 'ready', 'uuid' => $uuid]; 250 | $this->Clients[$SocketID]->uuid = $uuid; 251 | $this->Write($SocketID, json_encode($msg)); 252 | $Client->app->onOpen($SocketID); 253 | } 254 | } 255 | } 256 | 257 | public function Close($Socket) { 258 | if (is_int($Socket)) { 259 | $Socket = $this->Sockets[$Socket]; 260 | } 261 | stream_socket_shutdown($Socket, STREAM_SHUT_RDWR); 262 | $SocketID = intval($Socket); 263 | $this->onClose($SocketID); 264 | if ($this->maxPerIP > 0 && $this->Clients[$SocketID]->clientType == 'websocket') { 265 | $ip = $this->Clients[$SocketID]->ip; 266 | $this->clientIPs[$ip]->count--; 267 | if ($this->clientIPs[$ip]->count <= 0) { 268 | unset($this->clientIPs[$ip]); 269 | } 270 | } 271 | unset($this->Clients[$SocketID]); 272 | unset($this->Sockets[$SocketID]); 273 | return $SocketID; 274 | } 275 | 276 | private function extractMessage($SocketID) { 277 | $client = $this->Clients[$SocketID]; 278 | 279 | $message = $this->readDecode($SocketID); 280 | $opcode = $this->opcode; // opcode within from current frame 281 | $this->opcode = 1; // text , back to default; 282 | 283 | if ($opcode == 10) { //pong 284 | if ($client->expectPong == false) { 285 | $this->log("Unsolicited Pong frame received from socket #$SocketID $message"); // just ignore 286 | } else { 287 | $this->log("Expected Pong frame received from socket #$SocketID"); // just ignore 288 | $client->expectPong = false; 289 | } 290 | 291 | return ''; 292 | } 293 | if ($opcode == 9) { //ping received 294 | $this->log("Ping frame received from socket #$SocketID"); 295 | $this->opcode = 10; // pong 296 | $this->Write($SocketID, $message); 297 | $this->opcode = 1; 298 | return ''; 299 | } 300 | if ($opcode == 8) { //Connection Close Frame 301 | $this->log("Connection Close frame received from socket #$SocketID"); 302 | $this->Close($SocketID); 303 | return ''; 304 | } 305 | 306 | $this->Write($SocketID, json_encode((object) [ 307 | 'opcode' => 'next', 308 | 'fyi' => $this->Clients[$SocketID]->fyi])); 309 | /* 310 | * *********************************************** 311 | * take care of buffering messages either because 312 | * buffrerON===true or fin===false 313 | * *********************************************** 314 | */ 315 | if ($this->serverCommand($client, $message)) { 316 | return ''; 317 | } 318 | 319 | if ($client->bufferON) { 320 | if (count($client->buffer) <= $this->maxChunks) { 321 | $client->buffer[] = $message; 322 | } else { 323 | $this->log("Too many chunks from socket #$SocketID"); 324 | $this->onClose($SocketID); 325 | } 326 | return ''; 327 | } 328 | return $message; 329 | } 330 | 331 | public final function Write($SocketID, $message) { 332 | $m = $this->Encode($message); 333 | return fwrite($this->Sockets[$SocketID], $m, strlen($m)); 334 | } 335 | 336 | public final function feedback($packet) { 337 | foreach ($this->Clients as $client) { 338 | if (($packet->uuid == $client->uuid && $client->clientType === 'websocket') || 339 | ($packet->ident != '' && $packet->ident == $client->ident)) { 340 | $this->Write($client->ID, json_encode($packet)); 341 | return; 342 | } 343 | } 344 | } 345 | 346 | public final function echo($sockid, $packet) { 347 | $this->Write($sockid, json_encode($packet)); 348 | } 349 | 350 | public final function broadCast($SocketID, $M) { 351 | $ME = $this->Encode($M); 352 | foreach ($this->Clients as &$client) { 353 | if ($client->clientType === 'websocket') { 354 | if ($SocketID == $client->ID) { 355 | continue; 356 | } 357 | fwrite($this->Sockets[$client->ID], $ME, strlen($ME)); 358 | } 359 | } 360 | return; 361 | } 362 | 363 | public final function pingClients() { 364 | 365 | $this->opcode = 9; // PING 366 | $m = $this->Encode(json_encode((object) ['opcode' => 'PING'])); 367 | $this->opcode = 1; 368 | $nw = false; 369 | foreach ($this->Clients as &$client) { 370 | if ($client->clientType === 'websocket') { 371 | fwrite($this->Sockets[$client->ID], $m, strlen($m)); 372 | $client->expectPong = true; 373 | $nw = true; 374 | } 375 | } 376 | 377 | return $nw; 378 | } 379 | 380 | public final function registerResource($name, $app) { 381 | $this->allApps[$name] = $app; 382 | foreach (['registerServerMethods', 'onOpen', 'onData', 'onClose', 'onError'] as $method) { 383 | if (!method_exists($app, $method)) { 384 | $this->allApps[$name] = NULL; 385 | return false; 386 | } 387 | } 388 | $app->registerServerMethods($this); 389 | return true; 390 | } 391 | 392 | private function specificChecks($SocketID) { 393 | 394 | 395 | $Client = $this->Clients[$SocketID]; 396 | 397 | if ($Client->app === NULL) { 398 | $this->Log("Application incomplete or does not exist);" 399 | . " Telling Client to disconnect on #$SocketID"); 400 | $msg = (object) ['opcode' => 'close']; 401 | $this->Write($SocketID, json_encode($msg)); 402 | $this->Close($SocketID); 403 | return false; 404 | } 405 | 406 | if ($this->maxClients > 0 && count($this->Clients) > $this->maxClients) { 407 | $msg = "To many connections "; 408 | $this->Log("$SocketID, $msg"); 409 | $this->Write($SocketID, json_encode((object) ['opcode' => 'close', 'error' => $msg])); 410 | $this->Close($SocketID); 411 | return false; 412 | } 413 | 414 | if ($this->maxPerIP > 0 && $this->Clients[$SocketID]->clientType == 'websocket') { 415 | /* 416 | * *********************************************** 417 | * track number of websocket connectins from this IP 418 | * *********************************************** 419 | */ 420 | $ip = $Client->ip; 421 | if (!isset($this->clientIPs[$ip])) { 422 | $this->clientIPs[$ip] = (object) [ 423 | 'SocketId' => $SocketID, 424 | 'count' => 1 425 | ]; 426 | } else { 427 | $this->clientIPs[$ip]->count++; 428 | if ($this->clientIPs[$ip]->count > $this->maxPerIP) { 429 | $msg = "To many connections from: $ip"; 430 | $this->Log("$SocketID, $msg"); 431 | $this->Write($SocketID, json_encode((object) ['opcode' => 'close', 'error' => $msg])); 432 | $this->Close($SocketID); 433 | return false; 434 | } 435 | } 436 | } else if (count($this->allowedIP) > 0 && $this->Clients[$SocketID]->clientType != 'websocket') { 437 | /* 438 | * *********************************************** 439 | * check if tcp client connects from allowed host 440 | * *********************************************** 441 | */ 442 | if (!in_array($Client->ip, $this->allowedIP)) { 443 | $this->Close($SocketID); 444 | $this->Log("$SocketID, No connection allowed from: " . $Client->ip); 445 | return false; 446 | } 447 | } 448 | return true; 449 | } 450 | 451 | private function serverCommand($client, &$message) { 452 | if ($client->fin === true) { // no fragment 453 | if ($message === 'bufferON') { 454 | $client->bufferON = true; 455 | $client->buffer = []; 456 | $this->Log('Buffering ON'); 457 | return true; 458 | } 459 | 460 | if ($message === 'bufferOFF') { 461 | $client->bufferON = false; 462 | $message = implode('', $client->buffer); 463 | $client->buffer = []; 464 | $this->Log('Buffering OFF'); 465 | return false; 466 | } 467 | } 468 | if ($client->bufferON === false) { 469 | if ($client->fin === false && count($client->buffer) == 0) { 470 | $this->Log("FIN=false "); 471 | $client->buffer[] = $message; // a fragement 472 | return true; 473 | } 474 | if ($client->fin === true && count($client->buffer) > 0) { 475 | $client->buffer[] = $message; // last fragement 476 | $message = implode('', $client->buffer); 477 | $client->buffer = []; 478 | $this->Log('FIN=true'); 479 | } 480 | } 481 | 482 | return false; 483 | } 484 | 485 | public final function Log($m) { 486 | if ($this->logging) { 487 | $this->logging->log($m); 488 | } 489 | } 490 | 491 | public function guidv4() { 492 | 493 | // from https://www.uuidgenerator.net/dev-corner/php 494 | // Generate 16 bytes (128 bits) of random data or use the data passed into the function. 495 | $data = random_bytes(16); 496 | assert(strlen($data) == 16); 497 | // Set version to 0100 498 | $data[6] = chr(ord($data[6]) & 0x0f | 0x40); 499 | // Set bits 6-7 to 10 500 | $data[8] = chr(ord($data[8]) & 0x3f | 0x80); 501 | // Output the 36 character UUID. 502 | $unsecure = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); 503 | $token = ''; // generate whatever yo want 504 | $hash = password_hash($unsecure . $token, PASSWORD_DEFAULT, ["cost" => 5]); 505 | 506 | return $unsecure . $hash; 507 | } 508 | 509 | protected function verifyUUID($uuHash) { 510 | $uns = mb_substr($uuHash, 0, 36); 511 | $hash = mb_substr($uuHash, 36); 512 | $token = ''; // generate whatever yo want 513 | $f = password_verify($uns . $token, $hash); 514 | return $f; 515 | } 516 | 517 | function onClose($SocketID) { // ...socket has been closed AND deleted 518 | $this->Log("Connection closed to socket #$SocketID"); 519 | if ($this->Clients[$SocketID]->app == NULL) { 520 | return; 521 | } 522 | if (method_exists($this->Clients[$SocketID]->app, 'onClose')) { 523 | $this->Clients[$SocketID]->app->onClose($SocketID); 524 | } 525 | } 526 | 527 | function onError($SocketID, $message) { // ...any connection-releated error 528 | $this->Log("Socket $SocketID - " . $message); 529 | if ($this->Clients[$SocketID]->app == NULL) { 530 | return; 531 | } 532 | if (method_exists($this->Clients[$SocketID]->app, 'onError')) { 533 | $this->Clients[$SocketID]->app->onError($SocketID, $message); 534 | } 535 | } 536 | } 537 | --------------------------------------------------------------------------------