├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── examples ├── echo-server.php ├── fcgi.php ├── http-cookie.php ├── http-rest.php ├── http-route-adavanced.php ├── http-route.php ├── http.php ├── timeout.php ├── ws-chat │ ├── html │ │ └── index.html │ └── ws.php ├── ws-dynamic.php ├── ws-echo.php ├── ws-more.php └── ws-timeout.php ├── src ├── Attribute.php ├── Config.php ├── Console.php ├── EmitException.php ├── Event │ ├── EventEmitter.php │ ├── GlobalEventEmitter.php │ ├── Heap.php │ └── ResourceEventEmitter.php ├── FCGI │ ├── FCGI.php │ ├── FCGIServer.php │ ├── FCGIServerRequest.php │ ├── FCGIServerResponse.php │ └── FCGIUtil.php ├── GlobalConfig.php ├── HTTP │ ├── HTTP.php │ ├── HTTPRoute.php │ ├── HTTPServer.php │ ├── HTTPServerRequest.php │ ├── HTTPServerResponse.php │ └── HTTPUtil.php ├── Loop.php ├── NetConnection.php ├── Router │ ├── Matcher.php │ ├── PathMatcher.php │ └── Route.php ├── Server.php ├── ServerResponse.php ├── StreamSocket.php ├── Timeout.php └── WS │ ├── WS.php │ ├── WSApplication.php │ ├── WSFrame.php │ ├── WSNetConnection.php │ ├── WSServer.php │ ├── WSServerRequest.php │ ├── WSServerResponse.php │ └── WSUtil.php └── tests ├── EventEmitterTest.php ├── ResourceEventEmitterTest.php ├── RouteTest.php ├── ServerTest.php └── WSFrameTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryohei Nagatsuka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # EmitPHP 3 | 4 | EmitPHP is a PHP framework that works with non-blocking I/O. 5 | 6 | You can write your web applications and APIs in HTTP, FCGI and WebSocket. 7 | 8 | As of now, this is an experimental project and there still exists errors and bugs. 9 | 10 | ## Installation 11 | 12 | - It requires PHP 7(CLI) and composer to run 13 | - If you haven't installed composer, run below to install it. 14 | ```sh 15 | curl -sS https://getcomposer.org/installer | php 16 | sudo mv composer.phar /usr/local/bin/composer 17 | ``` 18 | 19 | - Download [EmitPHP source code](https://github.com/rnaga/EmitPHP.git) from github 20 | - Run composer to create autoload 21 | ```sh 22 | composer install 23 | ``` 24 | Now you can run [Examples](https://github.com/rnaga/EmitPHP/tree/master/examples) 25 | 26 | ## Usage 27 | ### WebSocket 28 | Yes, EmitPHP supports WebSocket. 29 | 30 | you can create your WebSocket Application with a few lines of code. 31 | 32 | Below is an example of how to create a WebSocket Application. 33 | 34 | ```php 35 | // Create a new WS Application 36 | $app = (new WSServer())->listen(4000)->app(); 37 | // Triggers when messages received 38 | $app->on('message', function($conn, $msg){ 39 | // Echo message 40 | $conn->send("echo => ". $msg); 41 | // Close the connection 42 | $conn->close(); 43 | }); 44 | 45 | \Emit\Loop(); 46 | ``` 47 | ### HTTP 48 | 49 | You can easily create a HTTP server as below 50 | ```php 51 | $server = (new HTTPServer())->listen(4000); 52 | $server->on('request', function($req, $res){ 53 | // Send response 54 | $res->send("Hello World"); 55 | // Close connection 56 | $res->end(); 57 | }); 58 | 59 | \Emit\Loop(); 60 | ``` 61 | ### Router 62 | Example for using Router 63 | 64 | ```php 65 | $server = (new HTTPServer())->listen(9000); 66 | // Create a new Route 67 | $route = $server->route(); 68 | // Get method 69 | $route->get("/", function($req, $res, $next){ 70 | $res->send("Hello World"); 71 | // Calling the next handler 72 | $next(); 73 | }); 74 | // Register the route 75 | $server->use($route); 76 | 77 | \Emit\Loop(); 78 | ``` 79 | ### FCGI 80 | EmitPHP supports FCGI which works with Web Servers such as apache 81 | ```php 82 | $server = (new FCGIServer())->listen(9000); 83 | $server->on('request', function($req, $res){ 84 | // Send response 85 | $res->send("Hello World"); 86 | // Close connection 87 | $res->end(); 88 | }); 89 | 90 | \Emit\Loop(); 91 | ``` 92 | 93 | See [examples](https://github.com/rnaga/EmitPHP/tree/master/examples) for more details. 94 | 95 | ## What's next 96 | 97 | Please send me your feeback at emitphp@gmail.com and let me know how you like it. 98 | If there are demands, I will work more. 99 | 100 | And if any of you wants to join the project, please let me know. 101 | 102 | ## License 103 | 104 | EmitPHP is licensed under the MIT license. See License File for more information. 105 | 106 | 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rnaga/emit-php", 3 | "description": "A PHP framework to develop WebSocket, HTTP and FCGI Applications", 4 | "license": "MIT", 5 | "keywords": ["websocket","http", "fcgi", "eventemitter"], 6 | "authors": [{ 7 | "name": "Ryohei Nagatsuka", 8 | "email": "ryoheinaga@gmail.com", 9 | "role": "Developer" 10 | }], 11 | "require": { 12 | "php": ">=7.0.0" 13 | }, 14 | "autoload": { 15 | "files": ["src/Loop.php"], 16 | "psr-4": { 17 | "Emit\\": "src/" 18 | } 19 | }, 20 | 21 | "autoload-dev": { 22 | "psr-4": { 23 | "EmitTest\\": "tests/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/echo-server.php: -------------------------------------------------------------------------------- 1 | listen(4000); 12 | 13 | $server->on('accept', function($sever, $resource){ 14 | 15 | $remote = new ResourceEventEmitter(); 16 | 17 | $remote->on("read", function($remote, $resource) { 18 | 19 | // Receives data 20 | $data = StreamSocket::read($resource); 21 | 22 | // Close the connection if disconnected by the client 23 | if( is_null( $data ) ) 24 | { 25 | $remote->close(); 26 | return; 27 | } 28 | 29 | // Echoes data to the client 30 | StreamSocket::write($resource, "echo => " . $data); 31 | 32 | })->listenResource($resource); 33 | }); 34 | 35 | \Emit\Loop(); 36 | -------------------------------------------------------------------------------- /examples/fcgi.php: -------------------------------------------------------------------------------- 1 | listen(9000); 16 | 17 | $server->on('request', function($req, $res){ 18 | 19 | // Send response 20 | $res->send("Hello World"); 21 | 22 | // Close connection 23 | $res->end(); 24 | 25 | }); 26 | 27 | \Emit\Loop(); 28 | 29 | -------------------------------------------------------------------------------- /examples/http-cookie.php: -------------------------------------------------------------------------------- 1 | listen(4000); 10 | 11 | $route = $server->route(); 12 | 13 | // Parse cookie when requested 14 | $route->get([$server, 'cookieParser']); 15 | 16 | $route->get(function($req, $res){ 17 | 18 | // Get Cookies 19 | $cookie = $req->cookie; 20 | 21 | // Set Cookie1 22 | $res->cookie('key1', 1, ['path' => '/']); 23 | $res->cookie('key2', 2, ['path' => '/']); 24 | 25 | $res->send('Cookie => ' . print_r( $cookie, 1 ) ); 26 | $res->end(); 27 | 28 | }); 29 | 30 | $server->use($route); 31 | 32 | \Emit\Loop(); 33 | 34 | -------------------------------------------------------------------------------- /examples/http-rest.php: -------------------------------------------------------------------------------- 1 | listen(4000); 11 | 12 | // Create new Route 13 | $route = $server->route(); 14 | 15 | // Mapping route to /resource/:id, where :id is [0-9]+ 16 | $server->use(['/resource/:id', ['id' => '[0-9]+']], $route); 17 | 18 | // Create Resource 19 | $route->post(function($req, $res){ 20 | $res->send("Create resource for " . $req->params['id']); 21 | $res->end(); 22 | }); 23 | 24 | // Read Resource 25 | $route->get(function($req, $res){ 26 | $res->send("Read resource for " . $req->params['id']); 27 | $res->end(); 28 | }); 29 | 30 | // Update Resource 31 | $route->put(function($req, $res){ 32 | $res->send("Update resource for " . $req->params['id']); 33 | $res->end(); 34 | }); 35 | 36 | // Delete Resource 37 | $route->delete(function($req, $res){ 38 | $res->send("Delete resource for " . $req->params['id']); 39 | $res->end(); 40 | }); 41 | 42 | // 404 for all others 43 | $server->on('request', function($req, $res){ 44 | $res->status(404); 45 | $res->send('File not found'); 46 | $res->end(); 47 | }); 48 | 49 | \Emit\Loop(); 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/http-route-adavanced.php: -------------------------------------------------------------------------------- 1 | listen(4000); 12 | 13 | // Create new Route 14 | $route = $server->route(); 15 | 16 | // Path with regex. It matches as /abc/1234/def/ 17 | $regex = PathMatcher::regex('([a-z]+)/([0-9]+)/(.+)'); 18 | 19 | $route->get($regex, function($req, $res, $next){ 20 | 21 | // Get parameters 22 | $params = $req->params; 23 | 24 | $res->send("Regex => " . print_r($params, 1)); 25 | $res->end(); 26 | }); 27 | 28 | // Set parameters for 'id' and 'name' with regex 29 | $route->get(['/:id/:name', ['id' => '[0-9]+', 'name' => '[a-z]+']], function($req, $res, $next ){ 30 | 31 | // Get parmameters 32 | $params = $req->params; 33 | 34 | $res->send("Parameters => " . print_r( $params, 1)); 35 | $res->end(); 36 | }); 37 | 38 | // Easier way to set a parameter 39 | $route->get('/:any', function($req, $res, $next ){ 40 | // Get parmameters 41 | $params = $req->params; 42 | 43 | $res->send("Parameters => " . print_r( $params, 1)); 44 | $res->end(); 45 | }); 46 | 47 | // How to retrieve request in body 48 | $route->post(function($req, $res, $next){ 49 | 50 | $body = $req->body; 51 | 52 | // Parse request -- or use json_decode for json format 53 | parse_str($body, $query); 54 | 55 | $res->send("Body => " . print_r( $query, 1)); 56 | $res->end(); 57 | }); 58 | 59 | // 404 for all other requests 60 | $route->all(function($req, $res){ 61 | $res->status(404); 62 | $res->send(''); 63 | $res->end(); 64 | }); 65 | 66 | // Register the route 67 | $server->use($route); 68 | 69 | \Emit\Loop(); 70 | 71 | -------------------------------------------------------------------------------- /examples/http-route.php: -------------------------------------------------------------------------------- 1 | listen(4000); 11 | 12 | // Create new Route 13 | $route = $server->route(); 14 | 15 | // Get method 16 | $route->get("/", function($req, $res, $next){ 17 | $res->send("Hello World"); 18 | // Calling the next handler 19 | $next(); 20 | }); 21 | 22 | // Post method 23 | $route->post("/", function($req, $res ){ 24 | $res->send("Hello World"); 25 | $res->end(); 26 | }); 27 | 28 | // Matching /abcd 29 | $route->get("/abcd", function($req, $res ){ 30 | $res->send("/abcd"); 31 | $res->end(); 32 | }); 33 | 34 | // 404 for all other requests 35 | $route->all(function($req, $res){ 36 | $res->status(404); 37 | $res->end(); 38 | }); 39 | 40 | // Register route 41 | $server->use($route); 42 | 43 | \Emit\Loop(); 44 | 45 | -------------------------------------------------------------------------------- /examples/http.php: -------------------------------------------------------------------------------- 1 | listen(4000); 10 | 11 | $server->on('request', function($req, $res){ 12 | 13 | // Send response 14 | $res->send("Hello World"); 15 | 16 | // Close connection 17 | $res->end(); 18 | 19 | }); 20 | 21 | \Emit\Loop(); 22 | 23 | -------------------------------------------------------------------------------- /examples/timeout.php: -------------------------------------------------------------------------------- 1 | 10) 20 | // Out of the loop 21 | return false; 22 | 23 | echo "Loop " . ($i++) . " times\n"; 24 | return true; 25 | 26 | }, 1000); 27 | 28 | \Emit\Loop(); 29 | 30 | -------------------------------------------------------------------------------- /examples/ws-chat/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WS Chat 6 | 7 | 8 | 60 | 61 | 132 | 133 | 134 | 135 | 138 |
139 |
140 |
141 |
142 | 143 | 144 | 145 |
146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /examples/ws-chat/ws.php: -------------------------------------------------------------------------------- 1 | listen(4001); 12 | 13 | $http->on('request', function($req, $res){ 14 | 15 | // Set Content-Type 16 | $res->setHeader('Content-Type', 'text/html'); 17 | 18 | // Get html file 19 | $html = file_get_contents(__DIR__ . '/html/index.html'); 20 | 21 | // Send it to the client 22 | $res->send($html); 23 | $res->end(); 24 | }); 25 | 26 | // Create WS Server 27 | $ws = (new WSServer())->listen(4000); 28 | 29 | // Triggers after WS handshake is done 30 | $ws->on('connect', function($req, $res){ 31 | 32 | $conn = $res->netConn; 33 | 34 | // Assign random ID and store it into the attribute 35 | $conn->attr('user_id', "user_" . rand()); 36 | }); 37 | 38 | // Create the new app 39 | $app = $ws->app('ws-chat'); 40 | 41 | // Triggers once when connection is estashlished 42 | $app->on('init', function($conn){ 43 | 44 | // Get the user id 45 | $userId = $conn->attr('user_id'); 46 | 47 | // Tell others someone joins the chat room 48 | $conn->connsForEach(function($theOther, $conn) use ($userId){ 49 | if( !$theOther->isMe($conn) ) { 50 | $theOther->send("$userId joins the chat room"); 51 | } 52 | }); 53 | }); 54 | 55 | // Triggers when message is received 56 | $app->on('message', function($conn, $msg){ 57 | 58 | // Get the user id 59 | $userId = $conn->attr('user_id'); 60 | 61 | // Broadcast message 62 | $conn->connsForEach(function($theOther, $conn) use ($msg, $userId){ 63 | if( !$theOther->isMe($conn) ) { 64 | $theOther->send("From $userId: $msg"); 65 | } 66 | }); 67 | }); 68 | 69 | 70 | // Triggers when connection is closed 71 | $app->on('close', function($conn){ 72 | 73 | // Get the user id 74 | $userId = $conn->attr('user_id'); 75 | 76 | // Tell others someone left the chat room 77 | $conn->connsForEach(function($theOther, $conn) use ($userId){ 78 | if( !$theOther->isMe($conn) ) { 79 | $theOther->send("$userId left the chat room"); 80 | } 81 | }); 82 | }); 83 | 84 | \Emit\Loop(); 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/ws-dynamic.php: -------------------------------------------------------------------------------- 1 | listen(4000); 10 | 11 | $ws->on('before_connect', function($ws, $req){ 12 | 13 | $appName = 'dynamic'; 14 | $req->appName = $appName; 15 | 16 | if( !is_null( $ws->getApp($appName) ) ) 17 | return; 18 | 19 | $app = $ws->app($appName); 20 | 21 | $app->on('message', function($conn, $msg){ 22 | 23 | // Echoes message 24 | $conn->send("echo => ". $msg); 25 | 26 | // Close the connection 27 | $conn->close(); 28 | }); 29 | }); 30 | 31 | \Emit\Loop(); 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/ws-echo.php: -------------------------------------------------------------------------------- 1 | listen(4000)->app(); 10 | 11 | $app->on('message', function($conn, $msg){ 12 | 13 | // Echoes message 14 | $conn->send("echo => ". $msg); 15 | 16 | // Close the connection 17 | $conn->close(); 18 | }); 19 | 20 | \Emit\Loop(); 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/ws-more.php: -------------------------------------------------------------------------------- 1 | listen(4000)->app(); 10 | 11 | $app->on('message', function($conn, $msg){ 12 | 13 | // Send text message 14 | $conn->send("Hello World"); 15 | 16 | // Send Fragmented messages 17 | $conn->sendFragBegin("Begin"); 18 | $conn->sendFrag(" ====== "); 19 | $conn->sendFragEnd( "End" ); 20 | 21 | // Send binary data 22 | $conn->sendBinary(pack('C', 0x01)); 23 | 24 | // Send binary fragmented data 25 | $conn->sendFragBinaryBegin(pack('C', 0x01)); 26 | $conn->sendFragBinary(pack('C', 0x02)); 27 | $conn->sendFragBinaryEnd(pack('C', 0x03)); 28 | 29 | // Broadcast message 30 | $conn->connsForEach(function($theOther, $conn) use ($msg){ 31 | if( !$theOther->isMe($conn) ) { 32 | $theOther->send("Received msg from " . $conn->getResource() . " msg:$msg"); 33 | } 34 | }); 35 | }); 36 | 37 | \Emit\Loop(); 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/ws-timeout.php: -------------------------------------------------------------------------------- 1 | listen(4000)->app(); 11 | 12 | $i = 0; 13 | 14 | $app->on('init', function($conn) use (&$i){ 15 | 16 | // Send Loop => $i for 10 times then close the connection 17 | Timeout::interval(function() use ($conn, &$i){ 18 | if( $i > 10 ) 19 | { 20 | $conn->send('bye'); 21 | $conn->close(); 22 | return false; 23 | } 24 | 25 | $conn->send("Loop => ". ($i++)); 26 | return true; 27 | }, 1000); 28 | }); 29 | 30 | \Emit\Loop(); 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Attribute.php: -------------------------------------------------------------------------------- 1 | attr[$key] = $value; 12 | } 13 | 14 | final public function getAttribute($key, $funcOrValue = null, ...$args) 15 | { 16 | if( !isset( $this->attr[$key] ) ) 17 | { 18 | if( is_callable( $funcOrValue ) ) 19 | $this->attr[$key] = $funcOrValue(...$args); 20 | else 21 | $this->attr[$key] = $funcOrValue; 22 | } 23 | 24 | return $this->attr[$key]; 25 | } 26 | 27 | final public function setAndGetAttribute($key, $funcOrValue = null, ...$args) 28 | { 29 | $value = $this->getAttribute($key); 30 | 31 | if( is_null( $attr ) ) 32 | { 33 | $value = $this->getAttribute($key, $funcOrValue, ...$args); 34 | if( is_null( $value ) ) return null; 35 | } 36 | 37 | $this->setAttribute($key, $value); 38 | return $value; 39 | } 40 | 41 | final public function unsetAttribute($key) 42 | { 43 | if( !isset( $this->attr[$key] ) ) 44 | unset($this->attr[$key]); 45 | } 46 | 47 | // jQuery Like 48 | final public function attr($key, $value = null) 49 | { 50 | if( is_null( $value ) ) 51 | { 52 | return $this->getAttribute($key); 53 | } 54 | 55 | $this->setAttribute($key, $value); 56 | return $this->getAttribute($key); 57 | } 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | attr = new Attribute(); 14 | 15 | foreach( $arr as $key => $value ) 16 | { 17 | $this->attr->setAttribute($key, $value); 18 | } 19 | } 20 | 21 | public static function read(array $arr) 22 | { 23 | $config = new Config($arr); 24 | return $config; 25 | } 26 | 27 | public static function readFile(string $file, string $var) 28 | { 29 | if( !is_readable( $file ) ) 30 | return null; 31 | 32 | require_once($file); 33 | 34 | if( isset( $$var ) && is_array( $$var ) ) 35 | return self::read($$var); 36 | 37 | return null; 38 | } 39 | 40 | public function get($key) 41 | { 42 | return $this->attr->getAttribute($key); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | 'DEBUG', 15 | self::LOG_NOTICE => 'NOTICE', 16 | self::LOG_WARN => 'WARN', 17 | self::LOG_ERROR => 'ERROR', 18 | ]; 19 | 20 | private static $level = self::LOG_ALL; 21 | 22 | public static function setLevel($level) 23 | { 24 | self::$level = $level; 25 | } 26 | 27 | public static function log($msg, $level = self::LOG_NOTICE) 28 | { 29 | if( 0 >= ( self::$level & $level ) ) 30 | return; 31 | 32 | $trace = debug_backtrace(); 33 | 34 | $ref = $trace[2]['class'] . "::" . $trace[2]['function']; 35 | 36 | $msg = str_replace("\n", "", $msg); 37 | $msg = date('Y-m-d H:i:s') . "\t" . self::LOG_TXT[$level] . "\t$ref\t$msg\n"; 38 | 39 | echo $msg; 40 | } 41 | 42 | public static function warn($msg) 43 | { 44 | self::log($msg, self::LOG_WARN); 45 | } 46 | 47 | public static function error($msg) 48 | { 49 | self::log($msg, self::LOG_ERROR); 50 | throw new EmitException($msg); 51 | } 52 | 53 | public static function debug($msg) 54 | { 55 | self::log($msg, self::LOG_DEBUG); 56 | } 57 | } 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/EmitException.php: -------------------------------------------------------------------------------- 1 | events = []; 16 | $this->id = GlobalEventEmitter::register($this); 17 | 18 | return $this; 19 | } 20 | 21 | function __destruct() 22 | { 23 | GlobalEventEmitter::notifyDestruct($this); 24 | } 25 | 26 | final public function has($eventName) 27 | { 28 | return isset( $this->events[$eventName] ); 29 | } 30 | 31 | final public function unset($eventName) 32 | { 33 | unset( $this->events[$eventName] ); 34 | } 35 | 36 | final public function on($eventName, $func) 37 | { 38 | $this->events[$eventName] = ['function' => $func]; 39 | return $this; 40 | } 41 | 42 | final public function emit($eventName, ...$args) 43 | { 44 | if( !$this->has($eventName) ) 45 | { 46 | Console::debug("EventName undefined $eventName"); 47 | return; 48 | } 49 | 50 | $emit = $this->events[$eventName]; 51 | return $emit['function'](...$args); 52 | } 53 | } 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Event/GlobalEventEmitter.php: -------------------------------------------------------------------------------- 1 | $ee, 21 | 'emits' => 0, 22 | 'id' => $id, 23 | ); 24 | 25 | return $id; 26 | } 27 | 28 | public static function notifyDestruct(EventEmitter $ee) 29 | { 30 | $id = $ee->id; 31 | unset( self::$eventList[$id] ); 32 | } 33 | 34 | public static function destroy(EventEmitter &$ee) 35 | { 36 | $id = $ee->id; 37 | unset(self::$eventList[$id]); 38 | 39 | if( $ee instanceof ResourceEventEmitter ) 40 | { 41 | $rId = $ee->getResource(); 42 | 43 | unset( self::$resources[$rId] ); 44 | unset( self::$eventResourceList[$rId] ); 45 | 46 | $ee->emit("resource_closed", $rId); 47 | } 48 | } 49 | 50 | public static function addNewResource(ResourceEventEmitter $ree) 51 | { 52 | $resource = $ree->getResource(); 53 | $rId = (int)$resource; 54 | 55 | self::$resources[$rId] = $resource; 56 | self::$eventResourceList[$rId] = [$ree]; 57 | } 58 | 59 | public static function setTimeout($function, int $timeout, bool $isLoop, ...$args) 60 | { 61 | if( !is_callable( $function ) ) return; 62 | 63 | $ee = new EventEmitter(); 64 | $ee->on('timeout', $function); 65 | 66 | $timeoutVal = [ 67 | 'ee' => $ee, 68 | 'unixTimeout' => (time() + (int)($timeout/1000)), 69 | 'timeout' => $timeout, 70 | 'isLoop' => $isLoop, 71 | 'args' => $args, 72 | ]; 73 | 74 | if( is_null( self::$eventTimeoutList ) ) 75 | { 76 | // Using min-heap for sorting the timeout list 77 | self::$eventTimeoutList = new Heap(function($t1, $t2){ 78 | $ut1 = $t1['unixTimeout']; 79 | $ut2 = $t2['unixTimeout']; 80 | 81 | return $ut2 - $ut1; 82 | }); 83 | } 84 | 85 | // O(log n) 86 | self::$eventTimeoutList->insert($timeoutVal); 87 | } 88 | 89 | public static function loop($usleep = 200 * 1000) 90 | { 91 | while(1){ 92 | 93 | if( is_null( self::$resources ) || !count( self::$resources ) ) 94 | { 95 | usleep($usleep); 96 | } 97 | else 98 | { 99 | $readSockets = array_values(self::$resources); 100 | $selectTimeout = $usleep; 101 | $write = $except = null; 102 | 103 | $n = stream_select( $readSockets, $write, $except, 0, $selectTimeout ); 104 | 105 | if( $n > 0 ) 106 | { 107 | foreach( $readSockets as $readSocket ) 108 | { 109 | $rId = (int)$readSocket; 110 | list($ree) = self::$eventResourceList[$rId]; 111 | $ree->emit("read", $ree, $readSocket); 112 | } 113 | } 114 | 115 | unset( $readSockets ); 116 | unset( $readSocket ); 117 | unset( $socket ); 118 | 119 | clearstatcache( ); 120 | } 121 | 122 | if( is_null( self::$eventTimeoutList ) ) 123 | continue; 124 | 125 | while( !self::$eventTimeoutList->isEmpty() ) 126 | { 127 | // O(1) 128 | $timeoutVal = self::$eventTimeoutList->top(); 129 | 130 | // ['ee', 'unixTimeout', 'timeout', 'isLoop', 'args'] 131 | extract($timeoutVal); 132 | 133 | if( time() < $unixTimeout ) 134 | break; 135 | 136 | $r = $ee->emit('timeout', ...$args); 137 | 138 | // O(log n) 139 | self::$eventTimeoutList->extract(); 140 | 141 | // Check to see if it's setInterval 142 | if( true === $r && $isLoop ) 143 | { 144 | // Reset timeout 145 | $timeoutVal['unixTimeout'] = time() + ($timeout/1000); 146 | self::$eventTimeoutList->insert($timeoutVal); 147 | } 148 | } 149 | } 150 | } 151 | 152 | } 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /src/Event/Heap.php: -------------------------------------------------------------------------------- 1 | comparator = $comparator; 12 | } 13 | 14 | public function compare($val1, $val2) 15 | { 16 | $comparator = $this->comparator; 17 | return $comparator($val1, $val2); 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/Event/ResourceEventEmitter.php: -------------------------------------------------------------------------------- 1 | has("read") ) 27 | { 28 | Console::warn("Must set 'read' event"); 29 | return; 30 | } 31 | 32 | $this->resource = $resource; 33 | GlobalEventEmitter::addNewResource($this); 34 | } 35 | 36 | public function getResource() 37 | { 38 | return $this->resource; 39 | } 40 | 41 | final public function close() 42 | { 43 | GlobalEventEmitter::destroy($this); 44 | StreamSocket::close($this->resource); 45 | $this->isClosed = true; 46 | } 47 | 48 | final public function isClosed() 49 | { 50 | return $this->isClosed; 51 | } 52 | } 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/FCGI/FCGI.php: -------------------------------------------------------------------------------- 1 | id = -1; 23 | $this->inLen = 0; 24 | $this->inPad = 0; 25 | $this->outHdr = NULL; 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/FCGI/FCGIServer.php: -------------------------------------------------------------------------------- 1 | setAttribute("server", $server); 24 | 25 | $remote->on("read", function($remote, $resource) use ($server){ 26 | 27 | $fcgi = $remote->getAttribute('fcgi'); 28 | 29 | if( is_null( $fcgi ) ) 30 | { 31 | $fcgi = FCGIUtil::initRequest( ); 32 | $remote->setAttribute('fcgi', $fcgi); 33 | 34 | list( $r, $error ) = FCGIUtil::readRequest( $resource, $fcgi ); 35 | 36 | if( !$r ) 37 | { 38 | StreamSocket::write($resource, $error."\n"); 39 | $remote->close(); 40 | return; 41 | } 42 | } 43 | else 44 | { 45 | $body = $remote->getAttribute("body"); 46 | 47 | $str = ""; 48 | $n = FCGIUtil::read( $resource, $fcgi, $str ); 49 | $body .= $str; 50 | 51 | if( $fcgi->inLen != 0 ) 52 | { 53 | $remote->setAttribute("body", $body); 54 | return; 55 | } 56 | 57 | if( $fcgi->env['REQUEST_URI'] == "" ) 58 | { 59 | Console::warn("REQUEST_URI undefined"); 60 | $remote->close(); 61 | return; 62 | } 63 | 64 | $server = $remote->getAttribute('server'); 65 | 66 | $netConn = new NetConnection($remote, $server); 67 | 68 | $request = new FCGIServerRequest($fcgi, $body); 69 | $response = new FCGIServerResponse($fcgi, $netConn); 70 | 71 | $method = $request->method; 72 | $requestURI = $request->requestURI; 73 | 74 | $route = $server->route; 75 | $route->dispatch($requestURI, $method, $request, $response); 76 | 77 | if( !$remote->isClosed() ) 78 | $server->emit("request", $request, $response); 79 | } 80 | })->listenResource($resource); 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/FCGI/FCGIServerRequest.php: -------------------------------------------------------------------------------- 1 | fcgi = $fcgi; 38 | $this->rawData = $body; 39 | $this->body = $body; 40 | 41 | $env = $fcgi->env; 42 | 43 | $this->method = HTTPUtil::getHeaderValue("REQUEST_METHOD", $env); 44 | $this->queryString = HTTPUtil::getHeaderValue("QUERY_STRING", $env); 45 | 46 | $protocol = HTTPUtil::getHeaderValue("SERVER_PROTOCOL", $env); 47 | list($this->protocol, $this->protocolVersion) = explode("/", $protocol); 48 | 49 | $this->method = HTTPUtil::getHeaderValue("REQUEST_METHOD", $env); 50 | $this->remoteHost = HTTPUtil::getHeaderValue("REMOTE_HOST", $env); 51 | $this->contentLength = HTTPUtil::getHeaderValue("CONTENT_LENGTH", $env); 52 | $this->cookie = HTTPUtil::getHeaderValue("COOKIE", $env); 53 | 54 | $this->requestURI = $env['REQUEST_URI']; 55 | $this->scriptName = $env['SCRIPT_NAME']; 56 | 57 | $this->rawHeaders = $env; 58 | } 59 | } 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/FCGI/FCGIServerResponse.php: -------------------------------------------------------------------------------- 1 | fcgi = $fcgi; 15 | } 16 | 17 | public function getStatusString() 18 | { 19 | return "Status: " . $this->status . "\r\n"; 20 | } 21 | 22 | public function _send( $data ) 23 | { 24 | $resource = $this->getResource(); 25 | 26 | $n = FCGIUtil::write($resource, $this->fcgi, FCGIRequestType::FCGI_STDOUT, $data, strlen( $data ) ); 27 | $n = FCGIUtil::flush( $resource, $this->fcgi, 0 ); 28 | return $n; 29 | } 30 | 31 | public function end( ) 32 | { 33 | $resource = $this->getResource(); 34 | 35 | $r = FCGIUtil::finishRequest( $resource, $this->fcgi, 1 ); 36 | $this->netConn->close(); 37 | } 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/FCGI/FCGIUtil.php: -------------------------------------------------------------------------------- 1 | version = FCGIUnpack( 'C', $data[0] ); 88 | $o->type = FCGIUnpack( 'C', $data[1] ); 89 | $o->requestIdB1 = FCGIUnpack( 'C', $data[2] ); 90 | $o->requestIdB0 = FCGIUnpack( 'C', $data[3] ); 91 | $o->contentLengthB1 = FCGIUnpack( 'C', $data[4] ); 92 | $o->contentLengthB0 = FCGIUnpack( 'C', $data[5] ); 93 | $o->paddingLength = FCGIUnpack( 'C', $data[6] ); 94 | $o->reserved = FCGIUnpack( 'C', $data[7] ); 95 | 96 | return $o; 97 | } 98 | 99 | public static function extract( $o ) 100 | { 101 | $data = array(); 102 | $data[0] = FCGIPack( 'C', $o->version ); 103 | $data[1] = FCGIPack( 'C', $o->type ); 104 | $data[2] = FCGIPack( 'C', $o->requestIdB1 ); 105 | $data[3] = FCGIPack( 'C', $o->requestIdB0 ); 106 | $data[4] = FCGIPack( 'C', $o->contentLengthB1 ); 107 | $data[5] = FCGIPack( 'C', $o->contentLengthB0 ); 108 | $data[6] = FCGIPack( 'C', $o->paddingLength ); 109 | $data[7] = FCGIPack( 'C', $o->reserved ); 110 | 111 | return join( "", $data ); 112 | } 113 | 114 | } 115 | 116 | final class FCGIBeginRequest 117 | { 118 | var /*unsigned char*/ $roleB1; 119 | var /*unsigned char*/ $roleB0; 120 | var /*unsigned char*/ $flags; 121 | var /*unsigned char*/ $reserved/*[5]*/; 122 | 123 | public static function sizeof(){ return 8;} 124 | public static function map( $data ) 125 | { 126 | $o = new FCGIBeginRequest( ); 127 | $o->roleB1 = FCGIUnpack( 'C', $data[0] ); 128 | $o->roleB0 = FCGIUnpack( 'C', $data[1] ); 129 | $o->flags = FCGIUnpack( 'C', $data[2] ); 130 | $o->reserved = FCGIUnpack( 'C*', $data[3] . $data[4] . $data[5] . $data[6] . $data[7] ); 131 | 132 | return $o; 133 | } 134 | } 135 | 136 | final class FCGIBeginRequestRec 137 | { 138 | var /*fcgi_header*/ $hdr; 139 | var /*fcgi_begin_request*/ $body; 140 | 141 | public static function sizeof(){ return FCGIHeader::sizeof() + FCGIBeginRequest::sizeof(); } 142 | } 143 | 144 | final class FCGIEndRequest 145 | { 146 | var /*unsigned char*/ $appStatusB3; 147 | var /*unsigned char*/ $appStatusB2; 148 | var /*unsigned char*/ $appStatusB1; 149 | var /*unsigned char*/ $appStatusB0; 150 | var /*unsigned char*/ $protocolStatus; 151 | var /*unsigned char*/ $reserved/*[3]*/; 152 | 153 | public static function sizeof(){ return 8; } 154 | 155 | public static function map( $data ) 156 | { 157 | $o = new FCGIEndRequest( ); 158 | $o->appStatusB3 = FCGIUnpack( 'C', $data[0] ); 159 | $o->appStatusB2 = FCGIUnpack( 'C', $data[1] ); 160 | $o->appStatusB1 = FCGIUnpack( 'C', $data[2] ); 161 | $o->appStatusB0 = FCGIUnpack( 'C', $data[3] ); 162 | $o->protocolStatus = FCGIUnpack( 'C', $data[4] ); 163 | $o->reserved = FCGIUnpack( 'C', $data[5] . $data[6] . $data[7] ); 164 | 165 | return $o; 166 | } 167 | 168 | public static function extract( $o ) 169 | { 170 | $data = array(); 171 | 172 | $data[0] = $o->appStatusB3; 173 | $data[1] = $o->appStatusB2; 174 | $data[2] = $o->appStatusB1; 175 | $data[3] = $o->appStatusB0; 176 | $data[4] = $o->protocolStatus; 177 | 178 | for( $i = 5; $i < 8; $i++ ) 179 | { 180 | $data[$i] = $o->reserved[($i-5)]; 181 | } 182 | 183 | return join( "", $data ); 184 | } 185 | } 186 | 187 | final class FCGIEndRequestRec 188 | { 189 | var /*fcgi_header*/ $hdr; 190 | var /*fcgi_end_request*/ $body; 191 | 192 | function __construct() 193 | { 194 | $this->body = new FCGIEndRequest(); 195 | } 196 | 197 | public static function sizeof(){ return FCGIHeader::sizeof() + FCGIBeginRequest::sizeof(); } 198 | 199 | public static function map( $data ) 200 | { 201 | $data_hdr = substr( $data, 0, FCGIHeader::sizeof( ) ); 202 | $data_body = substr( $data, FCGIHeader::sizeof( ) + 1, FCGIEndRequest::sizeof( ) ); 203 | 204 | $o = new FCGIEndRequestRec( ); 205 | $o->hdr = FCGIHeader::map( $data_hdr ); 206 | $o->body = FCGIEndRequest::map( $data_body ); 207 | 208 | return $o; 209 | } 210 | 211 | public static function extract( $o ) 212 | { 213 | return FCGIHeader::extract( $o->hdr ) . FCGIEndRequest::extract( $o->body ); 214 | } 215 | } 216 | 217 | final class FCGIUtil 218 | { 219 | public static function initRequest( ) 220 | { 221 | $req = new FCGI( ); 222 | return $req; 223 | } 224 | 225 | private static function readHeader($resource, FCGIHeader $hdr ) 226 | { 227 | //memset 228 | // $data = pack( 'C*', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ); 229 | 230 | $data = StreamSocket::read( $resource, FCGIHeader::sizeof() ); 231 | 232 | if( strlen( $data ) < 8 ) 233 | { 234 | return 0; 235 | } 236 | 237 | $hdr->version = FCGIUnpack( 'C', $data[0] ); 238 | $hdr->type = FCGIUnpack( 'C', $data[1] ); 239 | $hdr->requestIdB1 = FCGIUnpack( 'C', $data[2] ); 240 | $hdr->requestIdB0 = FCGIUnpack( 'C', $data[3] ); 241 | $hdr->contentLengthB1= FCGIUnpack( 'C', $data[4] ); 242 | $hdr->contentLengthB0= FCGIUnpack( 'C', $data[5] ); 243 | $hdr->paddingLength= FCGIUnpack( 'C', $data[6] ); 244 | $hdr->reserved= FCGIUnpack( 'C', $data[7] ); 245 | 246 | return 1; 247 | } 248 | 249 | private static function getParams(FCGI &$req, string $p, int $plen) 250 | { 251 | /*char*/ $buf/*[128]*/; 252 | $buf_size = 128; 253 | $name_len; $val_len; 254 | $s; 255 | $ret = 1; 256 | 257 | for( $i = 0; $i < $plen;) { 258 | 259 | $name_len = 0xff & FCGIUnpack( 'C', $p[$i++] ); 260 | if( $name_len >= 128) { 261 | $name_len = (($name_len & 0x7f) << 24); 262 | $name_len |= ( FCGIUnpack( 'C', $p[$i++] ) << 16); 263 | $name_len |= ( FCGIUnpack( 'C', $p[$i++] ) << 8); 264 | $name_len |= FCGIUnpack( 'C', $p[$i++] ); 265 | } 266 | 267 | $val_len = 0xff & FCGIUnpack( 'C', $p[$i++] ); 268 | if ($val_len >= 128) { 269 | $val_len = (($val_len & 0x7f) << 24); 270 | $val_len |= ( FCGIUnpack( 'C', $p[$i++] ) << 16); 271 | $val_len |= ( FCGIUnpack( 'C', $p[$i++] ) << 8); 272 | $val_len |= FCGIUnpack( 'C', $p[$i++] ); 273 | } 274 | 275 | if( $name_len + $val_len < 0 || 276 | $name_len + $val_len > $plen - $i) { 277 | /* Malformated request */ 278 | $ret = 0; 279 | break; 280 | } 281 | if ($name_len+1 >= $buf_size) { 282 | $buf_size = $name_len + 64; 283 | //tmp = (tmp == buf ? emalloc(buf_size): erealloc(tmp, buf_size)); 284 | } 285 | 286 | $tmp = substr( $p, $i, $name_len ); 287 | $s = substr( $p, $i + $name_len, $val_len ); //estrndup((char*)p + name_len, val_len); 288 | $req->env[$tmp] = $s; 289 | $i += $name_len + $val_len; 290 | } 291 | 292 | return $ret; 293 | } 294 | 295 | 296 | public static function readRequest($resource, FCGI $req ) 297 | { 298 | if( !is_resource( $resource ) ) 299 | { 300 | return array(0, "Passing invalid resource"); 301 | } 302 | 303 | $hdr = new FCGIHeader( ); 304 | 305 | $len = $padding = 0; 306 | 307 | $req->keep = 0; 308 | $req->closed = 0; 309 | $req->inLen = 0; 310 | $req->outHdr = null; 311 | 312 | self::readHeader( $resource, $hdr ); 313 | 314 | $len = ($hdr->contentLengthB1 << 8) | $hdr->contentLengthB0; 315 | $padding = $hdr->paddingLength; 316 | 317 | while( $hdr->type == FCGIRequestType::FCGI_STDIN && $len == 0 ) 318 | { 319 | if( 0 == self::readHeader( $resource, $hdr ) || $hdr->version < FCGI_VERSION_1 ) 320 | { 321 | //Logs::error( 'Fcgi Error: invliad header or version'); 322 | return array(0, 'Fcgi Error: invliad header or version'); 323 | } 324 | 325 | $len = ($hdr->contentLengthB1 << 8) | $hdr->contentLengthB0; 326 | $padding = $hdr->paddingLength; 327 | } 328 | 329 | if( $len + $padding > FCGI_MAX_LENGTH ) 330 | { 331 | //Logs::error( 'Fcgi Error: content length too long'); 332 | return array(0, 'Fcgi Error: content length too long. ' . ($len + $padding)); 333 | } 334 | 335 | $req->id = ($hdr->requestIdB1 << 8) + $hdr->requestIdB0; 336 | 337 | if( $hdr->type == FCGIRequestType::FCGI_BEGIN_REQUEST && 338 | $len == FCGIBeginRequest::sizeof() ) 339 | { 340 | $val = ""; 341 | 342 | $buf = StreamSocket::read( $resource, $len + $padding ); 343 | 344 | if( strlen( $buf ) != $len + $padding ) 345 | { 346 | //Logs::error( 'Fcgi Error: invalid content length'); 347 | return array(0, 'Fcgi Error: invalid content length'); 348 | } 349 | 350 | $fbr = FCGIBeginRequest::map( $buf ); 351 | 352 | $req->keep = ( $fbr->flags & FCGI_KEEP_CONN ); 353 | 354 | switch( ( $fbr->roleB1 << 8) + ( $fbr->roleB0 ) ) 355 | { 356 | case FCGIRole::FCGI_RESPONDER: 357 | $req->env["FCGI_ROLE"] = "RESPONDER"; 358 | break; 359 | case FCGIRole::FCGI_AUTHORIZER: 360 | $req->env["FCGI_ROLE"] = "AUTHORIZER"; 361 | break; 362 | case FCGIRole::FCGI_FILTER: 363 | $req->env["FCGI_ROLE"] = "FILTER"; 364 | break; 365 | default: 366 | //Logs::error( 'Fcgi Error: invalid fcgi role'); 367 | return array(0, 'Fcgi Error: invalid fcgi role'); 368 | } 369 | 370 | if( 0 == self::readHeader( $resource, $hdr ) || $hdr->version < FCGI_VERSION_1 ) 371 | { 372 | //Logs::error( 'Fcgi Error: invalid header or version 2'); 373 | return array(0, 'Fcgi Error: invalid header or version 2'); 374 | } 375 | 376 | $len = ($hdr->contentLengthB1 << 8) | $hdr->contentLengthB0; 377 | $padding = $hdr->paddingLength; 378 | 379 | while( $hdr->type == FCGIRequestType::FCGI_PARAMS && $len > 0 ) 380 | { 381 | if ($len + $padding > FCGI_MAX_LENGTH ) 382 | { 383 | //Logs::error( 'Fcgi Error: content length too long 2'); 384 | return array(0, 'Fcgi Error: content length too long 2'); 385 | } 386 | 387 | $buf = StreamSocket::read( $resource, $len + $padding ); 388 | 389 | if( strlen( $buf ) != $len + $padding ) 390 | { 391 | $req->keep = 0; 392 | //Logs::error( 'Fcgi Error: invalid content length'); 393 | return array(0, 'Fcgi Error: invalid content length'); 394 | } 395 | 396 | if( !self::getParams( $req, $buf, strlen($buf) ) ) 397 | { 398 | $req->keep = 0; 399 | //Logs::error( 'Fcgi Error: invalid parameters'); 400 | return array(0, 'Fcgi Error: invalid parameters'); 401 | } 402 | 403 | if( 0 == self::readHeader( $resource, $hdr ) || $hdr->version < FCGI_VERSION_1 ) 404 | { 405 | $req->keep = 0; 406 | //Logs::error( 'Fcgi Error: invalid header or version 2'); 407 | return array(0, 'Fcgi Error: invalid header or version 2'); 408 | } 409 | 410 | $len = ($hdr->contentLengthB1 << 8) | $hdr->contentLengthB0; 411 | $padding = $hdr->paddingLength; 412 | } 413 | 414 | } 415 | else if ($hdr->type == FCGIRequestType::FCGI_GET_VALUES ) 416 | { 417 | $buf = StreamSocket::read( $resource, $len + $padding ); 418 | 419 | if( strlen( $buf ) != $len + $padding ) 420 | { 421 | $req->keep = 0; 422 | //Logs::error( 'Fcgi Error: invalid content length 3'); 423 | return array(0, 'Fcgi Error: invalid content length 3'); 424 | } 425 | 426 | if( !self::getParams( $req, $buf, strlen($buf) ) ) 427 | { 428 | $req->keep = 0; 429 | //Logs::error( 'Fcgi Error: invalid parameters 2'); 430 | return array(0, 'Fcgi Error: invalid parameters 2'); 431 | } 432 | 433 | if( count( $req->env ) ) 434 | { 435 | $i = 0; 436 | foreach( $req->env as $key => $value ) 437 | { 438 | if( strlen( $value = FCGIGetValues::get($key) ) > 0 ) 439 | { 440 | $str_length = strlen( $key ); 441 | 442 | if( $str_length < 0x80 ) 443 | { 444 | $buf[$i++] = FCGIPack( 'C', $str_length ); 445 | } 446 | else 447 | { 448 | $buf[$i++] = FCGIPack( 'C', (($str_length >> 24) & 0xff) | 0x80 ); 449 | $buf[$i++] = FCGIPack( 'C', ($str_length >> 16) & 0xff ); 450 | $buf[$i++] = FCGIPack( 'C', ($str_length >> 8) & 0xff ); 451 | $buf[$i++] = FCGIPack( 'C', $str_length & 0xff ); 452 | } 453 | 454 | $zlen = strlen( $value ); 455 | 456 | if( $zlen < 0x80 ) 457 | { 458 | $buf[$i++] = $zlen; 459 | } 460 | else 461 | { 462 | $buf[$i++] = FCGIPack( 'C', (($zlen >> 24) & 0xff) | 0x80 ); 463 | $buf[$i++] = FCGIPack( 'C', ($zlen >> 16) & 0xff ); 464 | $buf[$i++] = FCGIPack( 'C', ($zlen >> 8) & 0xff ); 465 | $buf[$i++] = FCGIPack( 'C', $zlen & 0xff ); 466 | } 467 | 468 | $buf .= $key . $value; 469 | } 470 | } 471 | 472 | $buf .= self::makeHeader( $hdr, FCGIRequestType::FCGI_GET_VALUES_RESULT, 0, strlen( $buf ) ); 473 | $buf = FCGIHeader::extract( $hdr ) . $buf; 474 | 475 | if( StreamSocket::write( $resource, $buf, strlen( $buf ) ) != strlen( $buf ) ) 476 | { 477 | //Logs::error( 'Fcgi Error: socket write failed'); 478 | $req->keep = 0; 479 | } 480 | 481 | //Logs::error( 'Fcgi Error: FCGI_GET_VALUES called'); 482 | return array(0, 'Fcgi Error: FCGI_GET_VALUES called'); 483 | } 484 | 485 | //Logs::error( 'Fcgi Error: FCGI_GET_VALUES called without env'); 486 | return array(0, 'Fcgi Error: FCGI_GET_VALUES called without env'); 487 | } 488 | else 489 | { 490 | 491 | //Logs::error( 'Fcgi Error: unknown request type:' . $hdr->type ); 492 | return array(0, 'Fcgi Error: unknown request type:' . $hdr->type); 493 | } 494 | 495 | return array( 1, null ); 496 | } 497 | 498 | public static function read($resource, FCGI $req, &$str, int $len = 65536 ) 499 | { 500 | if( !is_resource( $resource ) ) 501 | { 502 | return 0; 503 | } 504 | 505 | $n = 0; 506 | $rest = $len; 507 | $hdr = new FCGIHeader( ); 508 | 509 | while( $rest > 0 ) 510 | { 511 | if( $req->inLen == 0 ) 512 | { 513 | if( 0 == self::readHeader( $resource, $hdr ) || $hdr->version < FCGI_VERSION_1 || 514 | $hdr->type != FCGIRequestType::FCGI_STDIN) 515 | { 516 | $req->keep = 0; 517 | return 0; 518 | } 519 | 520 | $req->inLen = ($hdr->contentLengthB1 << 8) | $hdr->contentLengthB0; 521 | $req->inPad = $hdr->paddingLength; 522 | 523 | if( $req->inLen == 0 ) 524 | { 525 | return $n; 526 | } 527 | } 528 | 529 | if( $req->inLen >= $rest ) 530 | { 531 | $tmp_str = StreamSocket::read( $resource, $rest); 532 | $ret = strlen( $tmp_str ); 533 | } 534 | else 535 | { 536 | $tmp_str = StreamSocket::read( $resource, $req->inLen ); 537 | $ret = strlen( $tmp_str ); 538 | } 539 | 540 | if( $ret < 0) 541 | { 542 | $req->keep = 0; 543 | return $ret; 544 | } 545 | else if( $ret > 0 ) 546 | { 547 | $req->inLen -= $ret; 548 | $rest -= $ret; 549 | $n += $ret; 550 | $str = $str.$tmp_str; 551 | 552 | if( $req->inLen == 0 ) 553 | { 554 | if( $req->inPad ) 555 | { 556 | $buf = StreamSocket::read( $resource, $req->inPad ); 557 | 558 | if( strlen( $buf ) != $req->inPad) 559 | { 560 | $req->keep = 0; 561 | return $ret; 562 | } 563 | } 564 | 565 | } 566 | else 567 | { 568 | return $n; 569 | } 570 | 571 | } 572 | else 573 | { 574 | return $n; 575 | } 576 | } 577 | 578 | return $n; 579 | } 580 | 581 | private static function makeHeader( &$hdr, $type, int $req_id, int $len ) 582 | { 583 | $paddingLength = (($len + 7) & ~7) - $len; 584 | $pad = array(); 585 | 586 | $hdr = new FCGIHeader( ); 587 | 588 | $hdr->contentLengthB0 = ($len & 0xff); 589 | $hdr->contentLengthB1 = (($len >> 8) & 0xff); 590 | $hdr->paddingLength = $paddingLength; 591 | $hdr->requestIdB0 = ($req_id & 0xff); 592 | $hdr->requestIdB1 = (($req_id >> 8) & 0xff); 593 | $hdr->reserved = 0x00; 594 | $hdr->type = $type; 595 | $hdr->version = FCGI_VERSION_1; 596 | 597 | if( $paddingLength ) 598 | { 599 | for( $i = 0; $i < $paddingLength; $i++ ) 600 | { 601 | $pad[] = FCGIPack( 'C', 0x00 ); 602 | } 603 | } 604 | 605 | return join("",$pad); 606 | } 607 | 608 | private static function openPacket(FCGI $req, $type ) 609 | { 610 | $req->outHdr = new FCGIHeader( ); 611 | $req->outHdr->type = $type; 612 | 613 | return $req->outHdr; 614 | } 615 | 616 | private static function closePacket(FCGI $req ) 617 | { 618 | if( $req->outHdr != null ) 619 | { 620 | $pad = self::makeHeader( $req->outHdr, $req->outHdr->type, $req->id, strlen( $req->outBuf ) ); 621 | $req->outBuf = FCGIHeader::extract( $req->outHdr ) . $req->outBuf . $pad; 622 | unset($req->outHdr); 623 | $req->outHdr = NULL; 624 | } 625 | } 626 | 627 | public static function flush($resource, FCGI $req, $close ) 628 | { 629 | if( !is_resource( $resource ) ) 630 | { 631 | return 0; 632 | } 633 | 634 | self::closePacket( $req ); 635 | $len = strlen( $req->outBuf ); 636 | 637 | if( $close ) 638 | { 639 | $rec = new FCGIEndRequestRec(); 640 | 641 | self::makeHeader( $rec->hdr, FCGIRequestType::FCGI_END_REQUEST, 642 | $req->id, FCGIEndRequest::sizeof() ); 643 | 644 | $rec->body->appStatusB3 = FCGIPack( 'C', 0x00 ); 645 | $rec->body->appStatusB2 = FCGIPack( 'C', 0x00 ); 646 | $rec->body->appStatusB1 = FCGIPack( 'C', 0x00 ); 647 | $rec->body->appStatusB0 = FCGIPack( 'C', 0x00 ); 648 | $rec->body->protocolStatus = FCGIPack( 'C', FCGIProtocolStatus::FCGI_REQUEST_COMPLETE); 649 | 650 | for( $i = 0; $i < 3; $i++ ) 651 | { 652 | $rec->body->reserved[$i] = FCGIPack( 'C', 0x00 ); 653 | } 654 | 655 | $req->outBuf .= FCGIEndRequestRec::extract( $rec ); 656 | 657 | $len += FCGIEndRequestRec::sizeof( ); 658 | 659 | unset( $rec ); 660 | } 661 | 662 | $n = StreamSocket::write( $resource, $req->outBuf, strlen( $req->outBuf ) ); 663 | 664 | if ( $n != $len ) 665 | { 666 | $req->keep = 0; 667 | return 0; 668 | } 669 | 670 | unset( $req->outBuf ); 671 | $req->outBuf = null; 672 | return 1; 673 | } 674 | 675 | public static function write($resource, FCGI $req, $type, $str, int $len ) 676 | { 677 | $buflen = 16384; 678 | 679 | if( $len <= 0 ) 680 | { 681 | return 0; 682 | } 683 | 684 | if( $req->outHdr && $req->outHdr->type != $type ) 685 | { 686 | self::closePacket( $req ); 687 | } 688 | 689 | $rest = $len; 690 | 691 | while( $rest > 0 ) 692 | { 693 | $limit = $buflen - strlen( $req->outBuf ); 694 | 695 | if( $req->outHdr == null ) 696 | { 697 | if( $limit < FCGIHeader::sizeof() ) 698 | { 699 | if ( !self::flush( $resource, $req, 0 ) ) 700 | { 701 | return -1; 702 | } 703 | } 704 | 705 | self::openPacket( $req, $type ); 706 | } 707 | 708 | $limit = $buflen - strlen($req->outBuf); 709 | 710 | if( $rest < $limit ) 711 | { 712 | //we have space in buffer 713 | $req->outBuf .= $str; 714 | return $len; 715 | } 716 | else 717 | { 718 | //no more buffer. send it 719 | $req->outBuf .= substr( $str, 0, $limit ); 720 | $rest -= $limit; 721 | $str = substr( $str, 0, $limit); 722 | 723 | if (!self::flush( $resource, $req, 0 ) ) 724 | { 725 | return -1; 726 | } 727 | } 728 | } 729 | 730 | return $len; 731 | } 732 | 733 | public static function finishRequest($resource, FCGI $req, $force_close ) 734 | { 735 | $ret = 1; 736 | if( is_null($req) || !is_resource( $resource ) ) return false; 737 | 738 | if( (int)( $resource ) >= 0 ) 739 | { 740 | if(! $req->closed ) 741 | { 742 | $ret = self::flush( $resource, $req, 1 ); 743 | $req->closed = 1; 744 | } 745 | 746 | //self::close( $req, $force_close, 1 ); 747 | } 748 | 749 | return $ret; 750 | } 751 | } 752 | -------------------------------------------------------------------------------- /src/GlobalConfig.php: -------------------------------------------------------------------------------- 1 | get($key); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/HTTP/HTTP.php: -------------------------------------------------------------------------------- 1 | [null, ['REQUEST_METHOD']], 59 | 'CONTENT_LENGTH' => [null, ['CONTENT_LENGTH', 'Content-Length']], 60 | 'COOKIE' => [null, ['HTTP_COOKIE', 'Cookie']], 61 | 'REMOTE_HOST' => [null, ['REMOTE_ADDR', 'X-Forwarded-Server']], 62 | 'USER_AGENT' => [null, ['HTTP_USER_AGENT', 'User-Agent']], 63 | 'QUERY_STRING' => [null, ['QUERY_STRING']], 64 | 'REQUEST_URI' => [null, ['REQUEST_URI']], 65 | 'HOST' => [null, ['HTTP_HOST', 'HOST']], 66 | 'ACCEPT' => [null, ['HTTP_ACCEPT', 'Accept']], 67 | 'ACCEPT_LANGUAGE' => ['null', ['HTTP_ACCEPT_LANGUAGE', 'Accept-Language']], 68 | 'ACCEPT_ENCODING' => [null, ['HTTP_ACCEPT_ENCODING', 'Accept-Encoding']], 69 | 'CONTENT_TYPE' => ['plain/text', ['CONTENT_TYPE', 'Content-Type']], 70 | 'SERVER_PROTOCOL' => ["HTTP/1.1", ['SERVER_PROTOCOL']], 71 | 'COOKIE' => [null, ['Cookie', 'HTTP_COOKIE']], 72 | ]; 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/HTTP/HTTPRoute.php: -------------------------------------------------------------------------------- 1 | route = []; 17 | 18 | foreach(['POST', 'GET', 'DELETE', 'HEAD', 'PUT', 'UPDATE'] as $method ) 19 | { 20 | $this->route[$method] = new Route(); 21 | } 22 | } 23 | 24 | public static function route() 25 | { 26 | return new Route(); 27 | } 28 | 29 | public function method($method, ...$args) 30 | { 31 | if( !isset( $this->route[$method] ) ) 32 | { 33 | return $this; 34 | } 35 | 36 | $route = $this->route[$method]; 37 | $route->use(...$args); 38 | return $this; 39 | } 40 | 41 | public function all(...$args) 42 | { 43 | foreach(['POST', 'GET', 'DELETE', 'HEAD', 'PUT', 'UPDATE'] as $method ) 44 | { 45 | $this->method($method, ...$args); 46 | } 47 | 48 | return $this; 49 | } 50 | 51 | public function post(...$args) 52 | { 53 | return $this->method('POST', ...$args); 54 | } 55 | 56 | public function put(...$args) 57 | { 58 | return $this->method('PUT', ...$args); 59 | } 60 | 61 | public function get(...$args) 62 | { 63 | return $this->method('GET', ...$args); 64 | } 65 | 66 | public function update(...$args) 67 | { 68 | return $this->method('UPDATE', ...$args); 69 | } 70 | 71 | public function delete(...$args) 72 | { 73 | return $this->method('DELETE', ...$args); 74 | } 75 | 76 | public function dispatch($path, &$matches, ...$args) 77 | { 78 | $method = $args[0]; 79 | $request = $args[1]; 80 | $response = $args[2]; 81 | 82 | $route = $this->route[$method]; 83 | 84 | $route->on("matches", function($matches, $handler, $request, $response, $next){ 85 | $request->params = $matches; 86 | $args = [$request, $response, $next]; 87 | $handler(...$args); 88 | }); 89 | 90 | $route->dispatch($path, $matches, $request, $response); 91 | $route->unset("matches"); 92 | } 93 | 94 | public function use(...$args) 95 | { 96 | return $this->all(...$args);; 97 | } 98 | } 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/HTTP/HTTPServer.php: -------------------------------------------------------------------------------- 1 | HTTPRoute 16 | protected $route; 17 | 18 | public function route() 19 | { 20 | return new HTTPRoute(); 21 | } 22 | 23 | public function use(...$args) 24 | { 25 | if( !$args[0] instanceof HTTPRoute && 26 | !$args[1] instanceof HTTPRoute ) 27 | { 28 | Console::error("Invalid arguments"); 29 | return null; 30 | } 31 | 32 | $this->route->use(...$args); 33 | } 34 | 35 | public static function cookieParser($request, $response, $next) 36 | { 37 | $cookie = HTTPUtil::getHeaderValue('COOKIE', $request->rawHeaders); 38 | $request->cookie = HTTPUTil::cookieToArray($cookie); 39 | $next(); 40 | } 41 | 42 | protected function onAccept($server, $resource) 43 | { 44 | $remote = new ResourceEventEmitter(); 45 | 46 | $remote->on("read", function($remote, $resource) use ($server) { 47 | 48 | $request = $remote->getAttribute("request", function(){ 49 | return HTTPUtil::initRequest(); 50 | }); 51 | 52 | $r = HTTPUtil::readRequest($resource, $request); 53 | 54 | if( $r === false ) 55 | { 56 | if( !is_null( $request->error ) ) 57 | { 58 | Console::warn($request->error); 59 | $server->emit("error", $remote, $resource); 60 | $remote->close(); 61 | } 62 | 63 | return; 64 | } 65 | 66 | $netConn = new NetConnection($remote, $server); 67 | $response = new HTTPServerResponse($request, $netConn); 68 | 69 | $method = $request->method; 70 | $requestURI = $request->requestURI; 71 | 72 | $route = $server->route; 73 | $route->dispatch($requestURI, $matches, $method, $request, $response); 74 | 75 | if( !$remote->isClosed() ) 76 | $server->emit("request", $request, $response); 77 | 78 | })->listenResource($resource); 79 | } 80 | 81 | protected function onClose($netConn) 82 | { 83 | $this->emit('close', $netConn); 84 | } 85 | 86 | function __construct() 87 | { 88 | parent::__construct(); 89 | 90 | $self = $this; 91 | $route = new Route(); 92 | 93 | $this->route = $route; 94 | 95 | $this->on('client_close', function($netConn) use ($self){ 96 | $self->onClose($netConn); 97 | }); 98 | 99 | $this->on('accept', function($server, $resource) use ($self){ 100 | $self->onAccept($server, $resource); 101 | }); 102 | } 103 | } 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/HTTP/HTTPServerRequest.php: -------------------------------------------------------------------------------- 1 | contentLength = 0; 29 | $this->env = array(); 30 | $this->maxRequestLength = $maxRequestLength; 31 | $this->error = null; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/HTTP/HTTPServerResponse.php: -------------------------------------------------------------------------------- 1 | request = $request; 19 | } 20 | 21 | public function getStatusString() 22 | { 23 | $request = $this->request; 24 | $statusCode = HTTP::responseCode($this->status); 25 | 26 | $statusString = $request->protocol . "/" . $request->protocolVersion 27 | . " " . $this->status . " $statusCode\r\n"; 28 | 29 | return $statusString; 30 | } 31 | 32 | public function _send( $data ) 33 | { 34 | $resource = $this->getResource(); 35 | 36 | $n = StreamSocket::write($resource, $data, strlen( $data ) ); 37 | return $n; 38 | } 39 | 40 | public function end( ) 41 | { 42 | $resource = $this->getResource(); 43 | $this->netConn->close(); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/HTTP/HTTPUtil.php: -------------------------------------------------------------------------------- 1 | $h) { 14 | $h = explode(':', $h, 2); 15 | 16 | if (isset($h[1])) { 17 | if (!isset($headers[$h[0]])) 18 | $headers[$h[0]] = trim($h[1]); 19 | elseif (is_array($headers[$h[0]])) { 20 | $headers[$h[0]] = array_merge($headers[$h[0]], array(trim($h[1]))); 21 | } 22 | else { 23 | $headers[$h[0]] = array_merge(array($headers[$h[0]]), array(trim($h[1]))); 24 | } 25 | 26 | $key = $h[0]; 27 | } 28 | else { 29 | if (substr($h[0], 0, 1) == "\t") 30 | $headers[$key] .= "\r\n\t".trim($h[0]); 31 | elseif (!$key) 32 | $headers[0] = trim($h[0]); 33 | } 34 | } 35 | 36 | return $headers; 37 | } 38 | } 39 | 40 | 41 | class HTTPUtil 42 | { 43 | // 2**20 as default maxRequestLength 44 | public static function initRequest($maxRequestLength = 1048576) 45 | { 46 | return new HTTPServerRequest($maxRequestLength); 47 | } 48 | 49 | public static function getHeaderValue(string $key, array $arrHeaders, $func = null) 50 | { 51 | $mapping = HTTP::HEADER_MAPPINGS; //HTTPMappings::HEADERS; 52 | 53 | if( !isset( $mapping[$key] ) ) 54 | return null; 55 | 56 | list($default, $keyList) = $mapping[$key]; 57 | 58 | foreach( $keyList as $k ) 59 | { 60 | if( isset( $arrHeaders[$k] ) ) 61 | return $arrHeaders[$k]; 62 | } 63 | 64 | if( !is_null( $func ) && is_callable( $func ) ) 65 | return $func; 66 | 67 | return $default; 68 | } 69 | 70 | public static function cookieToArray($str) 71 | { 72 | if( !is_string( $str ) ) 73 | { 74 | Console::debug('No Cookie'); 75 | return null; 76 | } 77 | 78 | $trimChars = ";\" \t\n\r\0\x0B"; 79 | 80 | for( $currPos = 0; strlen($str) > 0; ) 81 | { 82 | $pos = strpos($str, "="); 83 | if( false === $pos ) break; 84 | 85 | $key = trim(substr($str, 0, $pos), $trimChars); 86 | $str = substr($str, $pos+1); 87 | 88 | $delim = ( $str[0] == '"' ) ? '";' : ';'; 89 | 90 | $pos = strpos($str, $delim); 91 | 92 | if(false === $pos ) 93 | { 94 | $r[$key] = trim($str, $trimChars); 95 | break; 96 | } 97 | 98 | $value = trim(substr($str, 0, $pos), $trimChars); 99 | 100 | $str = substr($str, $pos+1); 101 | $r[$key] = $value; 102 | 103 | } 104 | 105 | return $r; 106 | } 107 | 108 | private static function readFirstLine(HTTPServerRequest $http) 109 | { 110 | $pos = strpos($http->rawData, "\r\n"); 111 | 112 | if( $pos === false ) 113 | return false; 114 | 115 | $firstLine = substr($http->rawData, 0, $pos); 116 | 117 | if( 3 != count( $arrFirstLine = explode(" ", $firstLine ) ) ) 118 | { 119 | $http->error = "Invalid Request $firstLine"; 120 | return false; 121 | } 122 | 123 | list($method, $uri, $protocol) = $arrFirstLine; 124 | 125 | if( !in_array( $method, ['POST', 'GET', 'DELETE', 'HEAD', 'PUT', 'UPDATE'] ) ) 126 | { 127 | $http->error = "Invalid method [$method]"; 128 | return false; 129 | } 130 | 131 | $http->parseURL = parse_url($uri); 132 | 133 | if( isset( $http->parseURL['query'] ) ) 134 | $http->queryString = $http->parseURL['query']; 135 | 136 | $http->requestURI = $uri; 137 | $http->scriptName = $http->parseURL['path']; 138 | 139 | $http->method = $method; 140 | 141 | list($http->protocol, $http->protocolVersion) = explode("/", $protocol); 142 | 143 | if( strlen($http->rawData) > ($pos + 2) ) 144 | $http->rawData = substr($http->rawData, $pos + 2); 145 | else 146 | $http->rawData = ""; 147 | 148 | $http->setAttribute('firstLineRead', true); 149 | 150 | return true; 151 | } 152 | 153 | public static function readHeader($resource, HTTPServerRequest $http) 154 | { 155 | $data = StreamSocket::read($resource, 16384); 156 | 157 | if( is_null( $data ) ) 158 | { 159 | $http->error = "Client Closed"; 160 | return false; 161 | } 162 | 163 | $http->rawData = $http->rawData . $data; 164 | 165 | if( !$http->getAttribute('firstLineRead', false) && 166 | false === self::readFirstLine($http) ) 167 | { 168 | return false; 169 | } 170 | 171 | $pos = strpos($http->rawData, "\r\n\r\n"); 172 | 173 | if( $pos === false ) 174 | { 175 | return false; 176 | } 177 | 178 | $headerTxt = substr($http->rawData, 0, $pos); 179 | $body = substr($http->rawData, $pos + 4); 180 | 181 | $rawHeaders = http_parse_headers($headerTxt); 182 | 183 | $http->rawHeaders = $rawHeaders; 184 | 185 | $http->remoteHost = StreamSocket::getRemoteHost($resource); 186 | $http->contentLength = (int)self::getHeaderValue('CONTENT_LENGTH', $rawHeaders); 187 | 188 | $cookie = self::getHeaderValue('COOKIE', $rawHeaders); 189 | 190 | //$http->cookie = self::cookieToArray($cookie); 191 | $http->body = $body; 192 | 193 | $http->setAttribute('headerRead', true); 194 | 195 | return true; 196 | } 197 | 198 | public static function readRequest($resource, HTTPServerRequest $http) 199 | { 200 | if( !$http->getAttribute('headerRead', false) ) 201 | { 202 | $r = self::readHeader($resource, $http); 203 | 204 | if( $r === false ) 205 | { 206 | return $r; 207 | } 208 | 209 | if( 0 == $http->contentLength || strlen($http->body) == $http->contentLength ) 210 | return true; 211 | 212 | return false; 213 | } 214 | 215 | $data = StreamSocket::read($resource, 1); 216 | 217 | if( is_null( $data ) ) 218 | { 219 | $http->error = 'connection closed'; 220 | return false; 221 | } 222 | 223 | if( 0 == $http->contentLength ) 224 | { 225 | $http->error = 'Invalid data received'; 226 | return false; 227 | } 228 | 229 | $http->body .= $data; 230 | 231 | if( $http->contentLength == strlen($http->body) ) 232 | return true; 233 | 234 | if( $http->contentLength < strlen($http->body) ) 235 | { 236 | $http->error = "Invalid Request. Content Length too long " . strlen($http->body); 237 | } 238 | 239 | return false; 240 | } 241 | } 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /src/Loop.php: -------------------------------------------------------------------------------- 1 | remote = $remote; 18 | $this->server = $server; 19 | } 20 | 21 | public function getRemote() 22 | { 23 | return $this->remote; 24 | } 25 | 26 | public function getResource() 27 | { 28 | return $this->remote->getResource(); 29 | } 30 | 31 | public function write($data, $blocking = 0) 32 | { 33 | $resource = $this->getResource(); 34 | $r = StreamSocket::write( $resource, $data, strlen($data), $blocking); 35 | return $r; 36 | } 37 | 38 | public function read($length = 8192, $blocking = 0) 39 | { 40 | $resource = $this->getResource(); 41 | $data = StreamSocket::read( $this->resource, $length, $blocking ); 42 | return $data; 43 | } 44 | 45 | public function close() 46 | { 47 | $this->server->emit("netconnection_closed", $this); 48 | $this->remote->close(); 49 | } 50 | } 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Router/Matcher.php: -------------------------------------------------------------------------------- 1 | isRegex = false; 19 | $this->varMapping = []; 20 | } 21 | 22 | private static function isValidRegex(string $regex) 23 | { 24 | if( false === @preg_match($regex, '' ) ) 25 | return false; 26 | 27 | return true; 28 | } 29 | 30 | public static function filter(string $path) 31 | { 32 | return trim(preg_replace("/\/[\/]+/", "/", $path), "/"); 33 | } 34 | 35 | public static function regex(string $regex) 36 | { 37 | $regex = "/^" . str_replace( '/', "\\/", $regex ) . "/"; 38 | 39 | if( !self::isValidRegex($regex) ) 40 | { 41 | Console::error("Invalid regex: $regex"); 42 | return false; 43 | } 44 | 45 | $matcher = new PathMatcher(); 46 | 47 | $matcher->isRegex = true; 48 | $matcher->pattern = $regex; 49 | 50 | return $matcher; 51 | } 52 | 53 | public static function compile($params = null) 54 | { 55 | if( is_null( $params ) ) $params = ''; 56 | 57 | // Only accepts string or array 58 | if( !is_string($params) && !is_array($params) ) 59 | { 60 | Console::error("Invalid Arguments"); 61 | return null; 62 | } 63 | 64 | if( is_string($params) ) 65 | { 66 | $path = $params; 67 | $regexes = []; 68 | } 69 | else // array 70 | { 71 | if( 2 < count($params) || !isset( $params[0] ) || !is_string($params[0]) ) 72 | { 73 | Console::error("Invalid Arguments"); 74 | return null; 75 | } 76 | 77 | $path = $params[0]; 78 | $regexes = isset($params[1]) ? $params[1] : []; 79 | } 80 | 81 | $matcher = new PathMatcher(); 82 | 83 | // Clean up slashes 84 | $matcher->pattern = $path = self::filter($path); 85 | 86 | if( $path == '' ) 87 | return $matcher; 88 | 89 | $pathArr = explode("/", $path); 90 | $varMapping = []; 91 | $isRegex = false; 92 | 93 | for( $i = 0, $matches = []; $i < count( $pathArr ); $i++, $matches = [] ) 94 | { 95 | $value = $pathArr[$i]; 96 | 97 | if( !@preg_match( self::$validFolderRegex, $value ) ) 98 | { 99 | Console::error("Invalid char $value"); 100 | return null; 101 | } 102 | 103 | if( $value[0] == ":" ) 104 | { 105 | if( false === @preg_match( self::$varMappingRegex, $value, $matches ) || 106 | !isset( $matches[1] ) ) 107 | { 108 | Console::error("Error $value"); 109 | return null; 110 | } 111 | 112 | $var = $matches[1]; 113 | $varMapping[] = $var; 114 | 115 | if( isset( $regexes[$var] ) ) 116 | $pathArr[$i] = "(" . $regexes[$var] . ")"; 117 | else 118 | $pathArr[$i] = self::$defaultRegex; 119 | 120 | $isRegex = true; 121 | } 122 | } 123 | 124 | if( !$isRegex ) 125 | { 126 | $matcher->pattern = implode("/", $pathArr ); 127 | return $matcher; 128 | } 129 | 130 | $regex = "/^" . implode("\/", $pathArr) . "/"; 131 | 132 | if( !self::isValidRegex( $regex ) ) 133 | { 134 | Console::error("Invalid regex: $regex"); 135 | return null; 136 | } 137 | 138 | $matcher->isRegex = $isRegex; 139 | $matcher->varMapping = $varMapping; 140 | $matcher->pattern = $regex; 141 | 142 | return $matcher; 143 | } 144 | 145 | public function substr($subject, $matches) 146 | { 147 | $subject = self::filter($subject); 148 | $str = $matches[0]; 149 | $newSubject = substr($subject, strlen($str)); 150 | return $newSubject; 151 | } 152 | 153 | public function matches($subject, &$matches) 154 | { 155 | // Accepts all paths 156 | if( $this->pattern == '' ) 157 | { 158 | $matches[0] = ''; 159 | return true; 160 | } 161 | 162 | $subject = self::filter($subject); 163 | 164 | if( !$this->isRegex ) 165 | { 166 | if( strlen($subject) >= strlen($this->pattern) && 167 | 0 === strpos( $subject, $this->pattern ) ) 168 | { 169 | $matches[0] = $this->pattern; 170 | return true; 171 | } 172 | 173 | return false; 174 | } 175 | 176 | $tmpMatches = []; 177 | 178 | $r = preg_match($this->pattern, $subject, $tmpMatches); 179 | 180 | if( $r == true && is_array( $this->varMapping ) ) 181 | { 182 | foreach( $tmpMatches as $i => $val ) 183 | { 184 | $matches[$i] = $tmpMatches[$i]; 185 | } 186 | 187 | for( $i = 0; $i < count($this->varMapping); $i++ ) 188 | { 189 | $var = $this->varMapping[$i]; 190 | $matches[$var] = $tmpMatches[$i+1]; 191 | unset( $matches[$i+1] ); 192 | } 193 | 194 | // Store the raw result 195 | $matches['__regex__'][] = $tmpMatches; 196 | } 197 | 198 | return $r; 199 | } 200 | } 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /src/Router/Route.php: -------------------------------------------------------------------------------- 1 | substr($subject, $matches); 35 | return $newSubject; 36 | } 37 | 38 | // Add a sub route 39 | public function use(...$args) 40 | { 41 | if( 0 >= count($args) ) 42 | { 43 | Console::error("No Arguments"); 44 | return false; 45 | } 46 | 47 | $moreArgs = []; 48 | 49 | if( 1 == count( $args ) ) 50 | { 51 | $pattern = null; 52 | $handler = $args[0]; 53 | } 54 | else 55 | { 56 | $pattern = $args[0]; 57 | $handler = $args[1]; 58 | } 59 | 60 | if( !$handler instanceof Route && 61 | !$this->isValidHandler($handler) && 62 | !is_callable( $handler ) ) 63 | { 64 | Console::error("Invalid Arguments"); 65 | return false; 66 | } 67 | 68 | if( $pattern instanceof Matcher ) 69 | $matcher = $pattern; 70 | else 71 | $matcher = $this->getMatcher($pattern); 72 | 73 | if( !$matcher instanceof Matcher ) 74 | { 75 | Console::error("Invalid Arguments. Route failed to create Matcher"); 76 | return false; 77 | } 78 | 79 | if( is_callable( $handler ) && count( $args ) > 2 ) 80 | { 81 | $moreArgs = array_slice($args, 2); 82 | } 83 | 84 | $this->vals[] = array 85 | ( 86 | 'matcher' => $matcher, 87 | 'handler' => $handler, 88 | 'more_args' => $moreArgs, 89 | ); 90 | 91 | return true; 92 | } 93 | 94 | public function dispatch($subject, &$matches, ...$args) 95 | { 96 | $vals = $this->vals; 97 | $self = $this; 98 | 99 | $next = function() use ($self, &$next, $subject, $args, &$vals, &$matches) 100 | { 101 | if( !count( $vals ) ) return; 102 | 103 | $nextVal = array_shift($vals); 104 | 105 | $matcher = $nextVal['matcher']; 106 | $handler = $nextVal['handler']; 107 | $moreArgs = $nextVal['more_args']; 108 | 109 | if( $matcher->matches( $subject, $matches ) ) 110 | { 111 | if( $handler instanceof Route ) 112 | { 113 | $handler->dispatch($self->sub($matcher, $subject, $matches), $matches, ...$args); 114 | $next(); 115 | return; 116 | } 117 | 118 | if( count( $moreArgs ) > 0 ) 119 | $args = array_merge($args, $moreArgs); 120 | 121 | $args[] = $next; 122 | 123 | $this->emit("matches", $matches, $handler, ...$args); 124 | return; 125 | } 126 | 127 | $next(); 128 | }; 129 | 130 | $next(); 131 | } 132 | } 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | config->get($key); 28 | } 29 | 30 | final public function listen(...$args) 31 | { 32 | if( !isset( $args[0] ) ) 33 | { 34 | Console::error('Invalid Arguments'); 35 | return; 36 | } 37 | 38 | if( is_string( $args[0] ) ) 39 | { 40 | if( !isset( $args[1] ) ) 41 | { 42 | Console::error('Invalid Arguments'); 43 | return; 44 | } 45 | 46 | $host = $args[0]; 47 | $port = $args[1]; 48 | } 49 | else 50 | { 51 | $host = '0.0.0.0'; 52 | $port = $args[0]; 53 | } 54 | 55 | if( !is_int( $port ) || !is_string( $host ) ) 56 | { 57 | throw new EmitException('Invalid Argument Types'); 58 | } 59 | 60 | Console::debug("Server listening on host: $host port: $port"); 61 | 62 | $resource = StreamSocket::createServerSocket( $host, $port ); 63 | 64 | if( !is_resource( $resource ) ) 65 | { 66 | Console::error('Failed to create a new resource'); 67 | return; 68 | } 69 | 70 | $this->on("read", function($server, $resource) { 71 | 72 | $remoteResource = StreamSocket::accept($resource); 73 | Console::debug("Accept new client rource: $remoteResource"); 74 | $server->emit("accept", $server, $remoteResource); 75 | 76 | })->listenResource($resource); 77 | 78 | return $this; 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/ServerResponse.php: -------------------------------------------------------------------------------- 1 | status = 200; 18 | $this->netConn = $netConn; 19 | $this->headerSent = false; 20 | $this->cookies = array(); 21 | 22 | $this->headers = array 23 | ( 24 | 'Content-Type' => 'text/plain', 25 | ); 26 | } 27 | 28 | abstract public function end(); 29 | abstract protected function _send($data); 30 | abstract protected function getStatusString(); 31 | 32 | public function getResource() 33 | { 34 | return $this->netConn->getResource(); 35 | } 36 | 37 | public function status($status) 38 | { 39 | $this->status = $status; 40 | } 41 | 42 | public function set($keyValues) 43 | { 44 | foreach( $keyValues as $k => $v ) 45 | { 46 | $this->append($k, $v); 47 | } 48 | } 49 | 50 | public function append($key, $value) 51 | { 52 | $this->headers[$key] = $value; 53 | return $this; 54 | } 55 | 56 | public function setHeader($key, $value) 57 | { 58 | return $this->append($key, $value); 59 | } 60 | 61 | public function unsetHeader($key) 62 | { 63 | unset( $this->headers[$key] ); 64 | } 65 | 66 | public function cookie($key, $value = "", $options = null) 67 | { 68 | $index = count($this->cookies); 69 | 70 | $this->cookies[$index] = array 71 | ( 72 | 'name' => $key, 73 | 'value' => $value, 74 | ); 75 | 76 | foreach( $options as $k => $v ) 77 | { 78 | if( preg_match( '/^(expires|domain|path|secure|httpOnly|maxAge|signed)$/', $k ) ) 79 | { 80 | $this->cookies[$index]['options'][$k] = $v; 81 | } 82 | } 83 | 84 | return $this; 85 | } 86 | 87 | public function getHeadersString() 88 | { 89 | $headerTxt = $this->getStatusString(); 90 | 91 | foreach( $this->headers as $k => $v ) 92 | { 93 | $headerTxt .= "$k: $v\r\n"; 94 | } 95 | 96 | foreach( $this->cookies as $cookie ) 97 | { 98 | $cookieTxt = "Set-Cookie: " 99 | . $cookie['name'] . "=" . $cookie['value'] ."; "; 100 | 101 | if( count($cookie['options']) ) 102 | { 103 | foreach( $cookie['options'] as $k => $v ) 104 | { 105 | $cookieTxt .= "$k=$v; "; 106 | } 107 | } 108 | 109 | $headerTxt .= "$cookieTxt\r\n"; 110 | } 111 | 112 | $headerTxt .= "\r\n"; 113 | 114 | return $headerTxt; 115 | } 116 | 117 | public function send($data) 118 | { 119 | $headerTxt = ""; 120 | 121 | $headerSent = $this->getAttribute("headerSent", false); 122 | 123 | if( !$headerSent ) 124 | { 125 | $headerTxt = $this->getHeadersString(); 126 | $this->setAttribute("headerSent", true); 127 | } 128 | 129 | $this->_send($headerTxt.$data); 130 | } 131 | 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/StreamSocket.php: -------------------------------------------------------------------------------- 1 | 0 ) 62 | { 63 | $data = stream_socket_recvfrom( $fd, $length ); 64 | } 65 | else 66 | { 67 | for(;;) 68 | { 69 | $tmp = stream_socket_recvfrom( $fd, 1024 ); 70 | if( $tmp == false ) break; 71 | 72 | $data .= $tmp; 73 | } 74 | } 75 | 76 | if( $blocking == 0 ) 77 | { 78 | stream_set_blocking( $fd, 1 ); 79 | } 80 | 81 | if( $data == "" ) return null; 82 | else return $data; 83 | 84 | } 85 | 86 | public static function writeBlocking( &$fd, $str, $len = 0 ) 87 | { 88 | return self::write( $fd, $str, $len, 1 ); 89 | } 90 | 91 | public static function write( &$fd, $str, $len = 0, $blocking = 0 ) 92 | { 93 | $len = ( $len == 0 ) ? strlen( $str ) : $len; 94 | $r = 0; 95 | 96 | 97 | if( $blocking == 0 ) 98 | { 99 | stream_set_blocking( $fd, 0 ); 100 | } 101 | 102 | do 103 | { 104 | $n = stream_socket_sendto( $fd, $str ); 105 | if( $n == false || 0 >= $n ) return -1; 106 | $len -= $n; $r += $n; 107 | 108 | } 109 | while( $len > 0 ); 110 | 111 | if( $blocking == 0 ) 112 | { 113 | stream_set_blocking( $fd, 1 ); 114 | } 115 | 116 | return $r; 117 | } 118 | 119 | public static function setTimeout( &$fd, $sec ) 120 | { 121 | stream_set_timeout($fd, $sec ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Timeout.php: -------------------------------------------------------------------------------- 1 | getConstants(); 58 | } 59 | 60 | foreach( self::$consts as $const => $value ) 61 | { 62 | if( 0 === strpos($const, $prefix) && $value == $targetValue ) 63 | { 64 | return $const; 65 | } 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/WS/WSApplication.php: -------------------------------------------------------------------------------- 1 | appName = $appName; 19 | } 20 | 21 | public function addNetConn(WSNetConnection $netConn) 22 | { 23 | $id = (int)$netConn->getResource(); 24 | $this->netConns[$id] = $netConn; 25 | $netConn->id = $id; 26 | 27 | Console::debug("Adding new netConn to WSApplication: $id"); 28 | } 29 | 30 | public function removeNetConn(WSNetConnection $netConn) 31 | { 32 | $id = (int)$netConn->getResource(); 33 | unset( $this->netConns[$id] ); 34 | 35 | Console::debug("Removing netConn from WSApplication: $id"); 36 | } 37 | 38 | public function addClient(ResourceEventEmitter $remote, WSNetConnection $netConn) 39 | { 40 | $remote->setAttribute('app', $this); 41 | $remote->setAttribute('appName', $this->appName); 42 | $remote->setAttribute("netConn", $netConn); 43 | 44 | $this->addNetConn($netConn); 45 | 46 | $self = $this; 47 | 48 | $remote->on('resource_closed', function($rId) use ($self, $netConn){ 49 | $self->removeNetConn($netConn); 50 | }); 51 | 52 | // Remove 'read' event which was set by the server 53 | $remote->unset('read'); 54 | 55 | // Re-set 'read' event 56 | $remote->on('read', function($remote, $resource) use ($self){ 57 | 58 | $netConn = $remote->getAttribute('netConn'); 59 | 60 | $frameIn = $netConn->frameIn; 61 | $frameOut = $netConn->frameOut; 62 | 63 | $resource = $remote->getResource(); 64 | 65 | $r = WSUtil::readFrame($resource, $frameIn); 66 | 67 | if( ( !$r && !is_null( $frameIn->error ) ) || $frameIn->opcode == WS::FRAME_OPCODE_CLOSE ) 68 | { 69 | $this->emit("close", $netConn, $frameIn); 70 | $remote->close(); 71 | 72 | Console::debug("Connection closed. Removing netConn from WSApplication: $netConn->id error:[" . $frameIn->error . "]"); 73 | 74 | return; 75 | } 76 | 77 | if( !$r ) 78 | { 79 | // Didin't get all frames 80 | return; 81 | } 82 | 83 | if( $frameIn->opcode == WS::FRAME_OPCODE_PONG ) 84 | { 85 | $this->emit("pong"); 86 | Console::debug("pong"); 87 | return; 88 | } 89 | 90 | if( $frameIn->opcode == WS::FRAME_OPCODE_PING ) 91 | { 92 | $this->emit("ping"); 93 | Console::debug("ping"); 94 | $netConn->sendPong(); 95 | return; 96 | } 97 | 98 | Console::debug("Frame received opcode: " . $frameIn->opcode . " payloadLen: " . $frameIn->payloadLen); 99 | 100 | $this->emit("message", $netConn, $frameIn->payload); 101 | $frameIn->reset(); 102 | }); 103 | 104 | $this->emit("init", $netConn); 105 | } 106 | } 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/WS/WSFrame.php: -------------------------------------------------------------------------------- 1 | error = 23 | $this->payload = 24 | $this->payloadLen = 25 | $this->closeStatus = 26 | $this->maskingKey = null; 27 | 28 | $this->opcode = 1; 29 | $this->fin = 1; 30 | 31 | $this->mask = 0x00; 32 | 33 | $this->buf = 34 | $this->bufPayload = ""; 35 | 36 | $this->isClosing = false; 37 | 38 | return $this; 39 | } 40 | 41 | public function setOptions(array $options) 42 | { 43 | foreach( $options as $key => $value ) 44 | { 45 | if( !is_string( $key ) ) 46 | continue; 47 | $this->setOption($key, $value); 48 | } 49 | 50 | return $this; 51 | } 52 | 53 | public function setOption(string $key, $value) 54 | { 55 | if( !property_exists($this, $key) ) 56 | return $this; 57 | 58 | $this->$key = $value; 59 | return $this; 60 | } 61 | 62 | public function options(array $options) 63 | { 64 | return $this->setOptions($options); 65 | } 66 | 67 | public function option(string $key, $value) 68 | { 69 | return $this->setOption($key, $value); 70 | } 71 | 72 | function __construct() 73 | { 74 | $this->reset(); 75 | return $this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/WS/WSNetConnection.php: -------------------------------------------------------------------------------- 1 | frameIn = WSUtil::initFrame(); 21 | $this->frameOut = WSUtil::initFrame(); 22 | 23 | $this->app = $app; 24 | } 25 | 26 | public function isMe(WSNetConnection $netConn) 27 | { 28 | return $netConn->id == $this->id; 29 | } 30 | 31 | public function connsForEach($func, ...$args) 32 | { 33 | if( !is_callable($func) ) return; 34 | 35 | $netConns = $this->app->netConns; 36 | 37 | foreach( $netConns as $id => $netConn ) 38 | { 39 | $func($netConn, $this, ...$args); 40 | } 41 | } 42 | 43 | public function broadCast($data) 44 | { 45 | $app = $this->app; 46 | $netConns = $app->netConns; 47 | 48 | foreach( $netConns as $id => $netConn ) 49 | { 50 | if( $this->equals($netConn) ) 51 | continue; 52 | 53 | $app->emit("broadcast", $netConn, $data); 54 | } 55 | } 56 | 57 | public function send($data, $isBinary = false) 58 | { 59 | $frameOut = $this->frameOut; 60 | $frameOut->reset(); 61 | 62 | $opcode = ($isBinary ) ? WS::FRAME_OPCODE_BINARY : WS::FRAME_OPCODE_TEXT; 63 | 64 | return $this->write($data, array('opcode' => $opcode)); 65 | } 66 | 67 | public function sendBinary($data) 68 | { 69 | return $this->send($data, true); 70 | } 71 | 72 | public function sendClose($closeStatus = WS::FRAME_CLOSE_NORMAL, $data = "closing") 73 | { 74 | $frameOut = $this->frameOut; 75 | $frameOut->reset(); 76 | 77 | $frameOut->opcode = WS::FRAME_OPCODE_CLOSE; 78 | $frameOut->closeStatus = $closeStatus; 79 | 80 | $params = array 81 | ( 82 | 'noReset' => true, 83 | 'opcode' => WS::FRAME_OPCODE_CLOSE, 84 | 'closeStatus' => $closeStatus, 85 | ); 86 | 87 | $this->write($data, $params); 88 | } 89 | 90 | // https://tools.ietf.org/html/rfc6455#section-5.4 91 | // 92 | // EXAMPLE: For a text message sent as three fragments, the first 93 | // fragment would have an opcode of 0x1 and a FIN bit clear, the 94 | // second fragment would have an opcode of 0x0 and a FIN bit clear, 95 | // and the third fragment would have an opcode of 0x0 and a FIN bit 96 | // that is set. 97 | // 98 | // https://tools.ietf.org/html/rfc6455#section-5.7 99 | // 100 | // A fragmented unmasked text message 101 | // * 0x01 0x03 0x48 0x65 0x6c (contains "Hel") 102 | // * 0x80 0x02 0x6c 0x6f (contains "lo") 103 | public function sendFragBegin($data, $isBinary = false) 104 | { 105 | $frameOut = $this->frameOut; 106 | $frameOut->reset(); 107 | 108 | $fin = 0; 109 | $opcode = ($isBinary ) ? WS::FRAME_OPCODE_BINARY : WS::FRAME_OPCODE_TEXT; 110 | 111 | $params = array 112 | ( 113 | 'noReset' => true, 114 | 'fin' => 0, 115 | 'opcode' => $opcode, 116 | ); 117 | 118 | return $this->write($data, $params); 119 | } 120 | 121 | public function sendFragBinaryBegin($data, $isBinary = false) 122 | { 123 | return $this->sendFragBegin($data, true); 124 | } 125 | 126 | public function sendFrag($data, $isEnd = false, $isBinary = false) 127 | { 128 | $frameOut = $this->frameOut; 129 | 130 | $params = array 131 | ( 132 | 'noReset' => true, 133 | 'fin' => ($isEnd) ? 1 : 0, 134 | 'opcode' => WS::FRAME_OPCODE_CONT, 135 | ); 136 | 137 | return $this->write($data, $params); 138 | } 139 | 140 | public function sendFragBinary($data, $isEnd = false) 141 | { 142 | return $this->sendFrag($data, $isEnd, true); 143 | } 144 | 145 | public function sendFragEnd($data, $isBinary = false) 146 | { 147 | return $this->sendFrag($data, true, $isBinary); 148 | } 149 | 150 | public function sendFragBinaryEnd($data) 151 | { 152 | return $this->sendFrag($data, true, true); 153 | } 154 | 155 | public function sendPing() 156 | { 157 | return self::write('', ['ping' => true]); 158 | } 159 | 160 | public function sendPong() 161 | { 162 | return self::write('', ['pong' => true]); 163 | } 164 | 165 | // @Override 166 | public function write($data, $params = null) 167 | { 168 | $frameOut = $this->frameOut; 169 | 170 | if( !isset($params['noReset']) ) 171 | $frameOut->reset(); 172 | 173 | $options = ['fin', 'opcode', 'closeStatus']; 174 | 175 | foreach( $options as $option ) 176 | { 177 | if( isset($params[$option]) ) 178 | $frameOut->$option = $params[$option]; 179 | } 180 | 181 | Console::debug("Sending data opcode: " . $frameOut->opcode . " payloadLen: " . $frameOut->payloadLen); 182 | 183 | if( isset( $params['pong'] ) ) 184 | $frameData = WSUtil::packPong($frameOut); 185 | else if( isset( $params['ping'] ) ) 186 | $frameData = WSUtil::packPing($frameOut); 187 | else 188 | $frameData = WSUtil::packFrame($frameOut, $data); 189 | 190 | return parent::write($frameData); 191 | } 192 | 193 | // @Override 194 | public function read($length = 16384, $blocking = 0) 195 | { 196 | $resource = $this->getResouce(); 197 | $frameIn = $this->frameIn; 198 | 199 | return WSUtil::readFrame($resource, $frameIn); 200 | } 201 | 202 | // @Override 203 | public function close($closeStatus = WS::FRAME_CLOSE_NORMAL) 204 | { 205 | $frameIn = $this->frameIn; 206 | 207 | if( !$frameIn->isClosing ) 208 | { 209 | $this->sendClose($closeStatus, "close"); 210 | } 211 | 212 | parent::close(); 213 | } 214 | } 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/WS/WSServer.php: -------------------------------------------------------------------------------- 1 | apps = []; 25 | 26 | $this->on('accept', function($server, $resource) { 27 | 28 | $remote = new ResourceEventEmitter(); 29 | 30 | $remote->on("read", function($remote, $resource) use ($server) { 31 | 32 | $request = $remote->getAttribute("request", function(){ 33 | return WSUtil::initRequest(); 34 | }); 35 | 36 | $resource = $remote->getResource(); 37 | 38 | $r = HTTPUtil::readRequest($resource, $request); 39 | 40 | if( $r === false ) 41 | { 42 | if( !is_null( $request->error ) ) 43 | { 44 | Console::warn($request->error); 45 | $this->emit("error", $remote, $resource); 46 | $remote->close(); 47 | } 48 | 49 | return; 50 | } 51 | 52 | if( $server->has('before_connect') ) 53 | $server->emit('before_connect', $server, $request); 54 | 55 | if( !is_null( $request->appName ) ) 56 | $appName = $request->appName; 57 | else 58 | // Get the appName from SCRIPT_NAME 59 | $appName = PathMatcher::filter($request->scriptName); 60 | 61 | $app = $this->getApp($appName); 62 | 63 | if( $app === null ) 64 | { 65 | Console::warn("App Not Found $appName"); 66 | $remote->close(); 67 | return; 68 | } 69 | 70 | $request->setWSHeaders(); 71 | 72 | $netConn = new WSNetConnection($remote, $this, $app); 73 | $response = new WSServerResponse($request, $netConn); 74 | 75 | $response->unsetHeader('Content-Type'); 76 | 77 | // Only supports RFC6455, version 13 78 | /* 79 | if( !isset( $request->secWebSocket['Version'] ) || 80 | false === strpos( $request->secWebSocket['Version'], '13' ) ) 81 | { 82 | $response->status(400); 83 | $response->send(); 84 | $remote->close(); 85 | return; 86 | } 87 | */ 88 | $this->emit("connect", $request, $response); 89 | 90 | if( !is_null( $request->error ) ) 91 | { 92 | Console::warn("Error: " . $request->error); 93 | $remote->close(); 94 | return; 95 | } 96 | 97 | $response->send(); 98 | $app->addClient($remote, $netConn); 99 | 100 | })->listenResource($resource); 101 | }); 102 | } 103 | 104 | public function app(string $appName = '') 105 | { 106 | if( $appName == '' ) 107 | $appName = self::$defaultAppName; 108 | else if( !preg_match( '/^[a-z][a-z0-9\/\-]+$/', $appName ) ) 109 | { 110 | Console::error("Illegal appName: $appName"); 111 | return null; 112 | } 113 | 114 | $appName = PathMatcher::filter($appName); 115 | 116 | if( $appName == '' ) 117 | return null; 118 | 119 | if( isset( $this->apps[$appName] ) ) 120 | return $this->apps[$appName]; 121 | 122 | $app = new WSApplication($appName); 123 | $this->apps[$appName] = $app; 124 | 125 | return $app; 126 | } 127 | 128 | public function getApp(string $appName) 129 | { 130 | if( $appName === '' ) 131 | $appName = self::$defaultAppName; 132 | 133 | if( !isset( $this->apps[$appName] ) ) 134 | { 135 | Console::error("App Not Found: $appName"); 136 | return null; 137 | } 138 | 139 | return $this->apps[$appName]; 140 | } 141 | 142 | } 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/WS/WSServerRequest.php: -------------------------------------------------------------------------------- 1 | contentLength = 0; 42 | $this->env = array(); 43 | $this->maxRequestLength = $maxRequestLength; 44 | $this->error = null; 45 | 46 | $this->secWebSocket = []; 47 | 48 | foreach( self::SEC_WEBSOCKET_KEYS as $key ) 49 | { 50 | $this->secWebSocket[$key] = ""; 51 | } 52 | } 53 | 54 | public function setWSHeaders() 55 | { 56 | foreach( $this->rawHeaders as $key => $value ) 57 | { 58 | // Accepts below 59 | // Sec-WebSocket-Key 60 | // Sec-WebSocket-Protocol 61 | // Sec-WebSocket-Version 62 | // Sec-WebSocket-Accept 63 | // Sec-WebSocket-Extensions 64 | // 65 | // 14 => strlen("Sec-WebSocket-") 66 | if( strlen($key) > 14 && substr($key, 0, 14) == "Sec-WebSocket-" ) 67 | { 68 | $seckey = substr($key, 14); 69 | 70 | if( in_array($seckey, self::SEC_WEBSOCKET_KEYS) ) 71 | { 72 | $this->secWebSocket[$seckey] = $value; 73 | } 74 | } 75 | else if( in_array($key, ['Origin', 'Connection', 'Upgrade']) ) 76 | { 77 | $key = strtolower($key); 78 | $this->$key = $value; 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/WS/WSServerResponse.php: -------------------------------------------------------------------------------- 1 | request = $request; 20 | $this->status = 101; 21 | 22 | // Add Headers as below 23 | // 24 | // Upgrade: websocket 25 | // Connection: Upgrade 26 | // Sec-WebSocket-Accept: $accept 27 | $this->append('Upgrade', 'websocket'); 28 | $this->append('Connection', 'Upgrade'); 29 | 30 | $key = $request->secWebSocket['Key']; 31 | $accept = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); 32 | 33 | $this->append('Sec-WebSocket-Accept', $accept); 34 | } 35 | 36 | public function getStatusString() 37 | { 38 | $request = $this->request; 39 | $statusString = $request->protocol . "/" . $request->protocolVersion . " "; 40 | 41 | if( $this->status == 101 ) 42 | { 43 | $statusString .= "101 Switching Protocols\r\n"; 44 | } 45 | else 46 | { 47 | $statusCode = HTTP::responseCode($this->status); 48 | $statusString .= $this->status . " $statusCode\r\n"; 49 | } 50 | 51 | return $statusString; 52 | } 53 | 54 | public function sendHeaders() 55 | { 56 | $this->send(); 57 | } 58 | 59 | // @Override 60 | public function send($data = null) 61 | { 62 | $headerSent = $this->getAttribute("headerSent", false); 63 | 64 | if( $headerSent ) return 0; 65 | 66 | $resource = $this->getResource(); 67 | 68 | // Send headers only 69 | $data = $this->getHeadersString(); 70 | 71 | $n = StreamSocket::write($resource, $data, strlen( $data ) ); 72 | 73 | $this->setAttribute("headerSent", true); 74 | 75 | return $n; 76 | } 77 | 78 | public function _send( $data ) 79 | { 80 | // Do nothing 81 | return; 82 | } 83 | 84 | public function end( ) 85 | { 86 | $resource = $this->getResource(); 87 | $this->netConn->close(); 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/WS/WSUtil.php: -------------------------------------------------------------------------------- 1 | opcode = WS::FRAME_OPCODE_PING; 33 | return self::packFrame($frame, ""); 34 | } 35 | 36 | public static function packPong(WSFrame $frame) 37 | { 38 | $frame->opcode = WS::FRAME_OPCODE_PONG; 39 | return self::packFrame($frame, ""); 40 | } 41 | 42 | public static function packFrame(WSFrame $frame, $payload) 43 | { 44 | $data = ""; 45 | 46 | if( !WS::isValidFrameOpcode($frame->opcode) ) 47 | { 48 | $frame->error = "Invalid opcode " . $frame->opcode; 49 | return null; 50 | } 51 | 52 | $opcode = $frame->opcode; 53 | $fin = $frame->fin << 7; 54 | 55 | // The first byte (Fin, RSV1-3 and Opcode) 56 | $data .= self::pack('C', $fin | ( 0x0f & $opcode) ); 57 | 58 | // The second byte (Mask, Payload Length) 59 | // 60 | // https://tools.ietf.org/html/rfc6455#section-5.1 61 | // A server MUST NOT mask any frames that it sends to the client. 62 | // A client MUST close a connection if it detects a masked frame. 63 | $binMask = ( $frame->mask == 0x01 ) ? 0x80 : 0x00; 64 | 65 | // Payload 66 | $payloadLen = strlen($payload); 67 | 68 | // https://tools.ietf.org/html/rfc6455#section-5.5.1 69 | // If there is a body, the first two bytes of 70 | // the body MUST be a 2-byte unsigned integer (in network byte order) 71 | if( $opcode == WS::FRAME_OPCODE_CLOSE ) 72 | { 73 | $payloadLen += 2; 74 | } 75 | 76 | if( $payloadLen > 65535 ) 77 | { 78 | // == 127 isn't available NOW 79 | $frame->error = "Payload too long " . $payloadLen; 80 | return false; 81 | } 82 | else if( $payloadLen < 126 ) 83 | { 84 | $binPayload = 0x7f & ( strlen($payload) ); 85 | $data .= self::pack('C', $binMask | $binPayload ); 86 | } 87 | else // == 126 88 | { 89 | $data .= self::pack('C', $binMask | 0x7e ) // 0x7e => 126 90 | // Multibyte length quantities are expressed in network byte order.(Big Endian) 91 | . self::pack('n', $payloadLen); // 2bytes, Big Endian 92 | } 93 | 94 | // Mask payload => This likely doesn't happen. See above 95 | if( $payloadLen > 0 && $frame->mask == 1 ) 96 | { 97 | for($i = 0, $maskingKey = ""; $i < 4; $i++) 98 | { 99 | $maskingKey .= self::pack('C', rand(0,255)); 100 | } 101 | 102 | $data .= $maskingKey; 103 | 104 | for( $i = 0; $i < strlen($payload); $i++) 105 | { 106 | $payload[$i] = $payload[$i]^$maskingKey[$i%4]; 107 | } 108 | } 109 | 110 | if( $opcode == WS::FRAME_OPCODE_CLOSE ) 111 | { 112 | $data .= self::pack( 'n', $frame->closeStatus ); 113 | } 114 | 115 | $data .= $payload; 116 | 117 | return $data; 118 | } 119 | 120 | public static function unpackFrame(WSFrame $frame, $data) 121 | { 122 | $data = $frame->buf . $data; 123 | 124 | $payloadOffset = 2; 125 | 126 | $bin = self::unpack( "C", $data[0]); 127 | 128 | $frame->fin = $bin >> 7; 129 | 130 | /* 131 | $rsv1 = ( $bin >> 6 ) & 0x01; 132 | $rsv2 = ( $bin >> 5 ) & 0x01; 133 | $rsv3 = ( $bin >> 4 ) & 0x01; 134 | */ 135 | 136 | $opcode = $frame->opcode = ( 0x0f & $bin ); 137 | 138 | if( !WS::isValidFrameOpcode($frame->opcode) ) 139 | { 140 | $frame->error = "Invalid opcode " . $frame->opcode; 141 | return false; 142 | } 143 | 144 | $bin = self::unpack( "C", $data[1]); 145 | 146 | $frame->mask = $bin >> 7 ; 147 | $payloadLen = 0x7f & $bin; 148 | 149 | if( $payloadLen == 0x7e ) // 126 150 | { 151 | $bin16 = substr( $data, 2, 2 ); 152 | $payloadLen = self::unpack( 'n', $bin16 ); 153 | $payloadOffset = 4; 154 | } 155 | else if( $frame->payloadLen == 0x7f ) // 127 156 | { 157 | $bin64 = substr( $data, 2, 8 ); 158 | $payloadLen = self::unpack( 'J', $bin64 ); 159 | $payloadOffset = 10; 160 | } 161 | 162 | if( $payloadLen > ( strlen($data) + $payloadOffset ) ) 163 | { 164 | $frame->buf = $data; 165 | return false; 166 | } 167 | 168 | $frame->payloadLen = $payloadLen; 169 | 170 | if( $payloadLen == 0 ) 171 | return $frame->fin == 1 ? true : false; 172 | 173 | $mask = $frame->mask; 174 | 175 | if( $mask == 1 ) 176 | { 177 | $frame->maskingKey = $maskingKey = substr($data, $payloadOffset, 4); 178 | $payloadOffset += 4; 179 | } 180 | 181 | $payload = substr($data, $payloadOffset, $payloadLen); 182 | 183 | if( $mask == 1 ) 184 | { 185 | for( $i = 0; $i < strlen($payload); $i++) 186 | { 187 | $payload[$i] = $payload[$i]^$maskingKey[$i%4]; 188 | } 189 | } 190 | 191 | if( $frame->fin == 0 ) 192 | { 193 | $frame->bufPayload .= $payload; 194 | return false; 195 | } 196 | 197 | $frame->payload = $frame->bufPayload . $payload; 198 | 199 | // 5.5.1. Close 200 | // 201 | // If there is a body, the first two bytes of 202 | // the body MUST be a 2-byte unsigned integer (in network byte order) 203 | if( $opcode == WS::FRAME_OPCODE_CLOSE ) 204 | { 205 | $bin16 = substr( $frame->payload, 0, 2 ); 206 | 207 | $frame->isClosing = true; 208 | 209 | $frame->closeStatus = self::unpack( 'n', $bin16 ); 210 | $closeStatusString = WS::getConstName("FRAME_CLOSE", $frame->closeStatus); 211 | 212 | if( !is_null($closeStatusString) ) 213 | $frame->closeStatusString = $closeStatusString; 214 | 215 | $frame->payload = substr($frame->payload, 2); 216 | } 217 | 218 | return true; 219 | } 220 | 221 | public static function readFrame($resource, WSFrame $frame) 222 | { 223 | $data = StreamSocket::read($resource); 224 | 225 | if( is_null( $data ) ) 226 | { 227 | $frame->error = "Connection Closed"; 228 | return false; 229 | } 230 | 231 | return self::unpackFrame($frame, $data); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /tests/EventEmitterTest.php: -------------------------------------------------------------------------------- 1 | e = new EventEmitter(); 15 | } 16 | 17 | public function testInstance() 18 | { 19 | $this->assertInstanceOf(EventEmitter::class, $this->e); 20 | } 21 | 22 | /** 23 | @depends testInstance 24 | */ 25 | public function testOn() 26 | { 27 | $e = $this->e; 28 | 29 | $e->on('new_event', function($params){}); 30 | 31 | $this->assertEquals(true, $e->has('new_event')); 32 | } 33 | 34 | /** 35 | @depends testInstance 36 | */ 37 | public function testUnSet() 38 | { 39 | $e = $this->e; 40 | 41 | $e->on('new_event', function($params){}); 42 | 43 | $this->assertEquals(true, $e->has('new_event')); 44 | 45 | $e->unset('new_event'); 46 | 47 | $this->assertEquals(false, $e->has('new_event')); 48 | } 49 | 50 | /** 51 | @depends testInstance 52 | */ 53 | public function testEmit() 54 | { 55 | $e = $this->e; 56 | 57 | $i = 1; 58 | 59 | $e->on('event', function() use (&$i){ 60 | $i = 2; 61 | }); 62 | 63 | // $i is updated after this 64 | $e->emit('event'); 65 | 66 | $this->assertEquals(2, $i); 67 | 68 | } 69 | } 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/ResourceEventEmitterTest.php: -------------------------------------------------------------------------------- 1 | e = new ResourceEventEmitter(); 16 | } 17 | 18 | public function testInstance() 19 | { 20 | $this->assertInstanceOf(ResourceEventEmitter::class, $this->e); 21 | } 22 | 23 | /** 24 | @depends testInstance 25 | */ 26 | public function testListenResource() 27 | { 28 | $e = $this->e; 29 | 30 | // Get warned if not resource 31 | $e->listenResource(null); 32 | 33 | $this->assertEquals(null, $e->getResource()); 34 | 35 | $resource = StreamSocket::createServerSocket('0.0.0.0', 4000); 36 | 37 | $e->listenResource($resource); 38 | 39 | // Need to define 'read' event 40 | $this->assertEquals(null, $e->getResource()); 41 | 42 | $e->on('read', function($e, $resource){}); 43 | 44 | $e->listenResource($resource); 45 | 46 | $this->assertEquals($resource, $e->getResource()); 47 | } 48 | } 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/RouteTest.php: -------------------------------------------------------------------------------- 1 | route = new Route(); 17 | $this->subRoute = new Route(); 18 | } 19 | 20 | public function testInstance() 21 | { 22 | $this->assertInstanceOf(Route::class, $this->route); 23 | $this->assertInstanceOf(Route::class, $this->subRoute); 24 | } 25 | 26 | /** 27 | @depends testInstance 28 | */ 29 | public function testUseFunction() 30 | { 31 | 32 | $route = $this->route; 33 | 34 | // Register function 35 | $route->use(function($params){ 36 | // Pass 37 | }); 38 | 39 | // With path 40 | $route->use("/path/to", function($params){ 41 | // Pass 42 | }); 43 | 44 | $subRoute = $this->subRoute; 45 | 46 | $subRoute->use(function($params){ 47 | 48 | }); 49 | 50 | // Add subRoute 51 | $route->use($subRoute); 52 | 53 | // PathMatcher 54 | $matcher = PathMatcher::regex("a/[0-9]/[a-b]/"); 55 | 56 | $route->use($matcher, function($params){ 57 | // Pass 58 | }); 59 | } 60 | 61 | /** 62 | @depends testUseFunction 63 | */ 64 | public function testDispatch() 65 | { 66 | $route = $this->route; 67 | $params = []; 68 | $route->dispatch("/", $matches, $params); 69 | } 70 | 71 | /** 72 | @depends testInstance 73 | @expectedException \Emit\EmitException 74 | */ 75 | public function testUseEmptyArgument() 76 | { 77 | $route = $this->route; 78 | $route->use(); 79 | } 80 | 81 | /** 82 | @depends testInstance 83 | @expectedException \Emit\EmitException 84 | */ 85 | public function testUseInvalidHandler() 86 | { 87 | $route = $this->route; 88 | $route->use("/path/to", new \stdClass()); 89 | } 90 | 91 | // Test for PatchMatcher 92 | 93 | public function testPatchMatcher() 94 | { 95 | $matcher = PathMatcher::compile("/"); 96 | $this->assertInstanceOf(PathMatcher::class, $matcher); 97 | 98 | // Checking for cleaning up slash 99 | $matcher = PathMatcher::compile("////a/////b///////////c"); 100 | $this->assertInstanceOf(PathMatcher::class, $matcher); 101 | 102 | $r = $matcher->matches("a/b/c", $matches); 103 | $this->assertEquals(true, $r); 104 | 105 | $matcher = PathMatcher::compile('/:id'); 106 | $this->assertInstanceOf(PathMatcher::class, $matcher); 107 | 108 | $r = $matcher->matches("/1234", $matches); 109 | $this->assertEquals(true, $r); 110 | 111 | $matcher = PathMatcher::compile(['/:id/:name', ['id' => '[0-9]+', 'name' => '[a-z]+']]); 112 | $this->assertInstanceOf(PathMatcher::class, $matcher); 113 | 114 | $r = $matcher->matches("/1234/name", $matches); 115 | $this->assertEquals(true, $r); 116 | 117 | $matcher = PathMatcher::regex('a/b/(.*)'); 118 | $this->assertInstanceOf(PathMatcher::class, $matcher); 119 | 120 | $r = $matcher->matches("a/b/c", $matches); 121 | $this->assertEquals(true, $r); 122 | } 123 | 124 | /** 125 | @depends testInstance 126 | @expectedException \Emit\EmitException 127 | */ 128 | public function testPatchMatcherInvalidRegex() 129 | { 130 | $matcher = PathMatcher::regex('a/b/(.*)////[]'); 131 | } 132 | 133 | /** 134 | @depends testInstance 135 | @expectedException \Emit\EmitException 136 | */ 137 | public function testPathMatcherInvalidArguments1() 138 | { 139 | // Doesn't allow integer 140 | $matcher = PathMatcher::compile(1234); 141 | } 142 | 143 | /** 144 | @depends testInstance 145 | @expectedException \Emit\EmitException 146 | */ 147 | public function testPathMatcherInvalidArguments2() 148 | { 149 | $matcher = PathMatcher::compile([]); 150 | } 151 | 152 | /** 153 | @depends testInstance 154 | @expectedException \Emit\EmitException 155 | */ 156 | public function testPathMatcherInvalidArguments3() 157 | { 158 | // Check for $varMappingRegex = "/^:([a-zA-Z]+(?:[a-zA-Z0-9_]*))$/"; 159 | $matcher = PathMatcher::compile('/:{]ksdsds'); 160 | } 161 | 162 | /** 163 | @depends testInstance 164 | @expectedException \Emit\EmitException 165 | */ 166 | public function testPathMatcherInvalidArguments4() 167 | { 168 | // Check for $varMappingRegex = "/^:([a-zA-Z]+(?:[a-zA-Z0-9_]*))$/"; 169 | $matcher = PathMatcher::compile('/a/[a-z]/(.*)/'); 170 | } 171 | 172 | } 173 | 174 | -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | listen(4000); 13 | $server2 = (new Server())->listen("0.0.0.0", 4001); 14 | } 15 | 16 | /** 17 | @expectedException \Emit\EmitException 18 | */ 19 | public function testInvalidArguments1() 20 | { 21 | $server = (new Server())->listen(1,1); 22 | } 23 | 24 | /** 25 | @expectedException \Emit\EmitException 26 | */ 27 | public function testInvalidArguments2() 28 | { 29 | $server = (new Server())->listen(); 30 | } 31 | 32 | /** 33 | @expectedException \Emit\EmitException 34 | */ 35 | public function testInvalidArguments3() 36 | { 37 | $server = (new Server())->listen(""); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/WSFrameTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(null, $frame->error); 20 | 21 | $frame2 = WSUtil::initFrame(); 22 | 23 | $unpack = WSUtil::unpackFrame($frame2, $pack); 24 | 25 | $this->assertEquals(true, $unpack); 26 | $this->assertEquals(strlen($data), $frame2->payloadLen); 27 | $this->assertEquals($data, $frame2->payload); 28 | } 29 | 30 | public function testFrameClose() 31 | { 32 | $data = "closing the connection"; 33 | 34 | $frame = WSUtil::initFrame() 35 | ->option("opcode", WS::FRAME_OPCODE_CLOSE) 36 | ->option("closeStatus", 1002); 37 | 38 | $pack = WSUtil::packFrame($frame, $data); 39 | 40 | $this->assertEquals(null, $frame->error); 41 | 42 | $frame2 = WSUtil::initFrame(); 43 | 44 | $unpack = WSUtil::unpackFrame($frame2, $pack); 45 | 46 | $this->assertEquals(true, $unpack); 47 | $this->assertEquals(1002, $frame2->closeStatus); 48 | $this->assertEquals(true, $frame2->isClosing); 49 | 50 | } 51 | 52 | } 53 | 54 | --------------------------------------------------------------------------------