├── .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 |
10 | - ssl://xyzabc.worldserver.net[:port]
11 |
- wss://xyzabc.worldserver.net[:port]
12 |
- ws://xyzabc.worldserver.net [:port]
13 |
- tcp://xyzabc.worldserver.net[:port]
14 |
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 | 
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 | 
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 | 
--------------------------------------------------------------------------------
/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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------