├── .gitignore ├── servicemanager ├── servicemanager.exe ├── servicemanager_mac ├── servicemanager_nix_32 ├── servicemanager_nix_64 ├── servicemanager_nix.systemd ├── servicemanager_mac.launchd └── servicemanager_nix.sysvinit ├── support ├── ras_functions.php ├── crc32_stream.php ├── str_basics.php ├── servicemanager.php ├── remotedapi_web_server.php ├── multi_async_helper.php ├── webroute.php ├── deflate_stream.php ├── random.php ├── cli.php ├── utf_utils.php ├── websocket_server.php └── webroute_server.php ├── tests ├── test_client.php └── test_server.php ├── sdks └── php │ ├── sdk_remotedapi.php │ ├── crc32_stream.php │ ├── webroute.php │ ├── deflate_stream.php │ ├── random.php │ └── utf_utils.php ├── install.php ├── README.md └── server.php /.gitignore: -------------------------------------------------------------------------------- 1 | /config.* 2 | /test.php 3 | -------------------------------------------------------------------------------- /servicemanager/servicemanager.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/remoted-api-server/master/servicemanager/servicemanager.exe -------------------------------------------------------------------------------- /servicemanager/servicemanager_mac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/remoted-api-server/master/servicemanager/servicemanager_mac -------------------------------------------------------------------------------- /servicemanager/servicemanager_nix_32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/remoted-api-server/master/servicemanager/servicemanager_nix_32 -------------------------------------------------------------------------------- /servicemanager/servicemanager_nix_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/remoted-api-server/master/servicemanager/servicemanager_nix_64 -------------------------------------------------------------------------------- /servicemanager/servicemanager_nix.systemd: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=@SERVICENAME@ 3 | After=syslog.target network.target remote-fs.target nss-lookup.target 4 | 5 | [Service] 6 | Type=forking 7 | PIDFile=@SERVICEPIDFILE@ 8 | ExecStart='@SERVICEMANAGER@' start '@SERVICENAME@' 9 | ExecReload='@SERVICEMANAGER@' reload '@SERVICENAME@' 10 | ExecStop='@SERVICEMANAGER@' stop '@SERVICENAME@' 11 | Restart=no 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /servicemanager/servicemanager_mac.launchd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.servicemanager.@SERVICENAME@ 7 | ProgramArguments 8 | 9 | @SERVICEMANAGER@ 10 | run 11 | @SERVICENAME@ 12 | 13 | KeepAlive 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /servicemanager/servicemanager_nix.sysvinit: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: @SERVICENAME@ 4 | # Required-Start: $local_fs $remote_fs $network 5 | # Required-Stop: $local_fs $remote_fs $network 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: starts @SERVICENAME@ 9 | # Description: starts @SERVICENAME@ 10 | ### END INIT INFO 11 | 12 | '@SERVICEMANAGER@' $1 '@SERVICENAME@' 13 | 14 | if [ "$?" != 0 ] ; then 15 | echo 16 | echo "Service Manager returned a failure code." 17 | echo 18 | echo "Usage: $0 {start|stop|restart|reload|status|configfile|custom-action-name|uninstall}" 19 | 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /support/ras_functions.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_client.php: -------------------------------------------------------------------------------- 1 | $fp 47 | ); 48 | 49 | $result = $web->Process($url . "/v1/some/api", $options); 50 | if (!$result["success"] || $result["response"]["code"] != 200) 51 | { 52 | var_dump($result); 53 | exit(); 54 | } 55 | 56 | var_dump($result["body"]); 57 | ?> -------------------------------------------------------------------------------- /sdks/php/sdk_remotedapi.php: -------------------------------------------------------------------------------- 1 | false, "error" => WebRoute::WRTranslate("Invalid Remoted API URL scheme."), "errorcode" => "invalid_scheme"); 45 | 46 | $result["url"] = $url; 47 | 48 | return $result; 49 | } 50 | 51 | if ($url2["loginusername"] === "") return array("success" => false, "error" => WebRoute::WRTranslate("Remoted API URL is missing client key."), "errorcode" => "missing_client_key"); 52 | 53 | $options["headers"]["X-Remoted-APIKey"] = $url2["loginusername"]; 54 | 55 | $url2["scheme"] = ($url2["scheme"] === "rwr" ? "wr" : "wrs"); 56 | unset($url2["loginusername"]); 57 | unset($url2["login"]); 58 | 59 | $url = HTTP::CondenseURL($url2); 60 | 61 | $result = $wr->Connect($url, false, $timeout, $options, $web); 62 | if (!$result["success"]) return $result; 63 | 64 | $options["fp"] = $result["fp"]; 65 | } 66 | 67 | return $result; 68 | } 69 | } 70 | ?> -------------------------------------------------------------------------------- /tests/test_server.php: -------------------------------------------------------------------------------- 1 | Start($host, 0, false); 30 | if (!$result["success"]) 31 | { 32 | var_dump($result); 33 | exit(); 34 | } 35 | 36 | echo "Ready.\n"; 37 | 38 | do 39 | { 40 | // Implement the stream_select() call directly when using multiple server instances. 41 | $timeout = 3; 42 | $readfps = array(); 43 | $writefps = array(); 44 | $exceptfps = NULL; 45 | 46 | $webserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 47 | 48 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 49 | if ($result === false) 50 | { 51 | if ($webserver instanceof RemotedAPIWebServer) 52 | { 53 | sleep(5); 54 | 55 | continue; 56 | } 57 | else 58 | { 59 | break; 60 | } 61 | } 62 | 63 | // Web server. 64 | $result = $webserver->Wait(0); 65 | 66 | // Handle active clients. 67 | foreach ($result["clients"] as $id => $client) 68 | { 69 | if (!isset($webtracker[$id])) 70 | { 71 | echo "Webserver client ID " . $id . " connected.\n"; 72 | 73 | $webtracker[$id] = array(); 74 | } 75 | 76 | if ($client->requestcomplete) 77 | { 78 | $result2 = array("success" => true, "message" => "It works!"); 79 | 80 | // Prevent proxies from doing bad things. 81 | $client->SetResponseNoCache(); 82 | 83 | $client->SetResponseCode(200); 84 | 85 | // Send the response. 86 | $client->SetResponseContentType("application/json"); 87 | $client->AddResponseContent(json_encode($result2)); 88 | $client->FinalizeResponse(); 89 | 90 | echo "Sending client " . $id . ": 'It works!'\n"; 91 | } 92 | } 93 | 94 | // Handle removed clients. 95 | foreach ($result["removed"] as $id => $result2) 96 | { 97 | if (isset($webtracker[$id])) 98 | { 99 | echo "Web server client ID " . $id . " disconnected.\n"; 100 | 101 | // echo "Client ID " . $id . " disconnected. Reason:\n"; 102 | // var_dump($result2["result"]); 103 | // echo "\n"; 104 | 105 | unset($webtracker[$id]); 106 | } 107 | } 108 | } while (1); 109 | ?> -------------------------------------------------------------------------------- /sdks/php/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /support/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /support/str_basics.php: -------------------------------------------------------------------------------- 1 | $val) 10 | { 11 | if (is_string($val)) $_REQUEST[$key] = trim($val); 12 | else if (is_array($val)) 13 | { 14 | $_REQUEST[$key] = array(); 15 | foreach ($val as $key2 => $val2) $_REQUEST[$key][$key2] = (is_string($val2) ? trim($val2) : $val2); 16 | } 17 | else $_REQUEST[$key] = $val; 18 | } 19 | } 20 | 21 | // Cleans up all PHP input issues so that $_REQUEST may be used as expected. 22 | public static function ProcessAllInput() 23 | { 24 | self::ProcessSingleInput($_COOKIE); 25 | self::ProcessSingleInput($_GET); 26 | self::ProcessSingleInput($_POST); 27 | } 28 | 29 | public static function ExtractPathname($dirfile) 30 | { 31 | $dirfile = str_replace("\\", "/", $dirfile); 32 | $pos = strrpos($dirfile, "/"); 33 | if ($pos === false) $dirfile = ""; 34 | else $dirfile = substr($dirfile, 0, $pos + 1); 35 | 36 | return $dirfile; 37 | } 38 | 39 | public static function ExtractFilename($dirfile) 40 | { 41 | $dirfile = str_replace("\\", "/", $dirfile); 42 | $pos = strrpos($dirfile, "/"); 43 | if ($pos !== false) $dirfile = substr($dirfile, $pos + 1); 44 | 45 | return $dirfile; 46 | } 47 | 48 | public static function ExtractFileExtension($dirfile) 49 | { 50 | $dirfile = self::ExtractFilename($dirfile); 51 | $pos = strrpos($dirfile, "."); 52 | if ($pos !== false) $dirfile = substr($dirfile, $pos + 1); 53 | else $dirfile = ""; 54 | 55 | return $dirfile; 56 | } 57 | 58 | public static function ExtractFilenameNoExtension($dirfile) 59 | { 60 | $dirfile = self::ExtractFilename($dirfile); 61 | $pos = strrpos($dirfile, "."); 62 | if ($pos !== false) $dirfile = substr($dirfile, 0, $pos); 63 | 64 | return $dirfile; 65 | } 66 | 67 | // Makes an input filename safe for use. 68 | // Allows a very limited number of characters through. 69 | public static function FilenameSafe($filename) 70 | { 71 | return preg_replace('/\s+/', "-", trim(trim(preg_replace('/[^A-Za-z0-9_.\-]/', " ", $filename), "."))); 72 | } 73 | 74 | public static function ReplaceNewlines($replacewith, $data) 75 | { 76 | $data = str_replace("\r\n", "\n", $data); 77 | $data = str_replace("\r", "\n", $data); 78 | $data = str_replace("\n", $replacewith, $data); 79 | 80 | return $data; 81 | } 82 | 83 | public static function LineInput($data, &$pos) 84 | { 85 | $CR = ord("\r"); 86 | $LF = ord("\n"); 87 | 88 | $result = ""; 89 | $y = strlen($data); 90 | if ($pos > $y) $pos = $y; 91 | while ($pos < $y && ord($data[$pos]) != $CR && ord($data[$pos]) != $LF) 92 | { 93 | $result .= $data[$pos]; 94 | $pos++; 95 | } 96 | if ($pos + 1 < $y && ord($data[$pos]) == $CR && ord($data[$pos + 1]) == $LF) $pos++; 97 | if ($pos < $y) $pos++; 98 | 99 | return $result; 100 | } 101 | 102 | // Constant-time string comparison. Ported from CubicleSoft C++ code. 103 | public static function CTstrcmp($secret, $userinput) 104 | { 105 | $sx = 0; 106 | $sy = strlen($secret); 107 | $uy = strlen($userinput); 108 | $result = $sy - $uy; 109 | for ($ux = 0; $ux < $uy; $ux++) 110 | { 111 | $result |= ord($userinput[$ux]) ^ ord($secret[$sx]); 112 | $sx = ($sx + 1) % $sy; 113 | } 114 | 115 | return $result; 116 | } 117 | 118 | public static function ConvertUserStrToBytes($str) 119 | { 120 | $str = trim($str); 121 | $num = (double)$str; 122 | if (strtoupper(substr($str, -1)) == "B") $str = substr($str, 0, -1); 123 | switch (strtoupper(substr($str, -1))) 124 | { 125 | case "P": $num *= 1024; 126 | case "T": $num *= 1024; 127 | case "G": $num *= 1024; 128 | case "M": $num *= 1024; 129 | case "K": $num *= 1024; 130 | } 131 | 132 | return $num; 133 | } 134 | 135 | public static function ConvertBytesToUserStr($num) 136 | { 137 | $num = (double)$num; 138 | 139 | if ($num < 0) return "0 B"; 140 | if ($num < 1024) return number_format($num, 0) . " B"; 141 | if ($num < 1048576) return str_replace(".0 ", "", number_format($num / 1024, 1)) . " KB"; 142 | if ($num < 1073741824) return str_replace(".0 ", "", number_format($num / 1048576, 1)) . " MB"; 143 | if ($num < 1099511627776.0) return str_replace(".0 ", "", number_format($num / 1073741824.0, 1)) . " GB"; 144 | if ($num < 1125899906842624.0) return str_replace(".0 ", "", number_format($num / 1099511627776.0, 1)) . " TB"; 145 | 146 | return str_replace(".0 ", "", number_format($num / 1125899906842624.0, 1)) . " PB"; 147 | } 148 | 149 | public static function JSSafe($data) 150 | { 151 | return str_replace(array("'", "\r", "\n"), array("\\'", "\\r", "\\n"), $data); 152 | } 153 | } 154 | ?> -------------------------------------------------------------------------------- /install.php: -------------------------------------------------------------------------------- 1 | array( 22 | "s" => "suppressoutput", 23 | "?" => "help" 24 | ), 25 | "rules" => array( 26 | "suppressoutput" => array("arg" => false), 27 | "help" => array("arg" => false) 28 | ) 29 | ); 30 | $args = CLI::ParseCommandLine($options); 31 | 32 | if (isset($args["opts"]["help"])) 33 | { 34 | echo "Remoted API server installation command-line tool\n"; 35 | echo "Purpose: Installs the remoted API server from the command-line.\n"; 36 | echo "\n"; 37 | echo "This tool is question/answer enabled. Just running it will provide a guided interface.\n"; 38 | echo "\n"; 39 | echo "Syntax: " . $args["file"] . " [options]\n"; 40 | echo "Options:\n"; 41 | echo "\t-s Suppress most output. Useful for capturing JSON output.\n"; 42 | echo "\n"; 43 | echo "Examples:\n"; 44 | echo "\tphp " . $args["file"] . "\n"; 45 | 46 | exit(); 47 | } 48 | 49 | $config = RAS_LoadConfig(); 50 | 51 | $suppressoutput = (isset($args["opts"]["suppressoutput"]) && $args["opts"]["suppressoutput"]); 52 | 53 | function DisplayResult($result) 54 | { 55 | echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 56 | 57 | exit(); 58 | } 59 | 60 | if (!isset($config["server_apikey"])) 61 | { 62 | $rng = new CSPRNG(true); 63 | $config["server_apikey"] = $rng->GenerateString(64); 64 | 65 | RAS_SaveConfig($config); 66 | } 67 | 68 | if (!isset($config["client_apikey"])) 69 | { 70 | $rng = new CSPRNG(true); 71 | $config["client_apikey"] = $rng->GenerateString(64); 72 | 73 | RAS_SaveConfig($config); 74 | } 75 | 76 | if (!isset($config["host"])) 77 | { 78 | $ipv6 = CLI::GetYesNoUserInputWithArgs($args, "ipv6", "IPv6", "N", "The next question asks if a localhost IPv6 server should be started instead of the default localhost IPv4 server. A web server will proxy requests to this server.", $suppressoutput); 79 | 80 | $config["host"] = ($ipv6 ? "[::1]" : "127.0.0.1"); 81 | 82 | RAS_SaveConfig($config); 83 | } 84 | 85 | if (!isset($config["port"])) 86 | { 87 | $port = (int)CLI::GetUserInputWithArgs($args, "port", "Port", "37791", "The next question asks what port number to run the server on. A web server will proxy requests to this server and port.", $suppressoutput); 88 | if ($port < 0 || $port > 65535) $port = 37791; 89 | $config["port"] = $port; 90 | 91 | RAS_SaveConfig($config); 92 | } 93 | 94 | if (function_exists("posix_geteuid")) 95 | { 96 | $uid = posix_geteuid(); 97 | if ($uid !== 0) CLI::DisplayError("The Remoted API Server installer must be run as the 'root' user (UID = 0) to install the system service on *NIX hosts."); 98 | } 99 | 100 | if (!isset($config["serviceuser"])) 101 | { 102 | if (!function_exists("posix_geteuid")) $config["serviceuser"] = ""; 103 | else 104 | { 105 | $serviceuser = CLI::GetUserInputWithArgs($args, "serviceuser", "System service user/group", "remoted-api-server", "The next question asks what user the system service will run as. Both a system user and group will be created.", $suppressoutput); 106 | 107 | $config["serviceuser"] = $serviceuser; 108 | 109 | // Create the system user/group. 110 | ob_start(); 111 | system("useradd -r -s /bin/false " . escapeshellarg($serviceuser)); 112 | if ($suppressoutput) ob_end_clean(); 113 | else ob_end_flush(); 114 | } 115 | 116 | RAS_SaveConfig($config); 117 | } 118 | 119 | // Allow the user to read the configuration. 120 | if ($config["serviceuser"] !== "") @chown($rootpath . "/config.dat", $config["serviceuser"]); 121 | 122 | if (!isset($config["servicename"])) 123 | { 124 | $servicename = CLI::GetUserInputWithArgs($args, "servicename", "System service name", "remoted-api-server", "The next question asks what the name of the system service will be. Enter a single hyphen '-' to not install this software as a system service at this time.", $suppressoutput); 125 | 126 | if ($servicename !== "-") 127 | { 128 | $config["servicename"] = $servicename; 129 | 130 | RAS_SaveConfig($config); 131 | 132 | // Install and start 'server.php' as a system service. 133 | if (!$suppressoutput) echo "\nInstalling system service...\n"; 134 | ob_start(); 135 | system(escapeshellarg(PHP_BINARY) . " " . escapeshellarg($rootpath . "/server.php") . " install"); 136 | system(escapeshellarg(PHP_BINARY) . " " . escapeshellarg($rootpath . "/server.php") . " start"); 137 | echo "\n\n"; 138 | if ($suppressoutput) ob_end_clean(); 139 | else ob_end_flush(); 140 | } 141 | } 142 | 143 | $result = array( 144 | "success" => true, 145 | "config" => $config, 146 | "configfile" => $rootpath . "/config.dat" 147 | ); 148 | 149 | DisplayResult($result); 150 | ?> -------------------------------------------------------------------------------- /support/servicemanager.php: -------------------------------------------------------------------------------- 1 | rootpath = str_replace(array("\\", "/"), DIRECTORY_SEPARATOR, $rootpath); 12 | } 13 | 14 | public function Install($servicename, $phpfile, $args, $options = array(), $display = false) 15 | { 16 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 17 | if (!isset($options["dir"])) $options["dir"] = $this->rootpath; 18 | foreach ($options as $key => $val) $cmd .= " " . escapeshellarg("-" . $key . "=" . $val); 19 | $cmd .= " install " . escapeshellarg($servicename); 20 | $cmd .= " " . escapeshellarg($phpfile . ".notify"); 21 | $cmd .= " " . escapeshellarg($this->GetPHPBinary()); 22 | $cmd .= " " . escapeshellarg($phpfile); 23 | foreach ($args as $arg) $cmd .= " " . escapeshellarg($arg); 24 | 25 | return $this->RunCommand($cmd, $display); 26 | } 27 | 28 | public function Uninstall($servicename, $display = false) 29 | { 30 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 31 | $cmd .= " uninstall " . escapeshellarg($servicename); 32 | 33 | return $this->RunCommand($cmd, $display); 34 | } 35 | 36 | public function Start($servicename, $display = false) 37 | { 38 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 39 | $cmd .= " start " . escapeshellarg($servicename); 40 | 41 | return $this->RunCommand($cmd, $display); 42 | } 43 | 44 | public function Stop($servicename, $display = false) 45 | { 46 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 47 | $cmd .= " stop " . escapeshellarg($servicename); 48 | 49 | return $this->RunCommand($cmd, $display); 50 | } 51 | 52 | public function Restart($servicename, $display = false) 53 | { 54 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 55 | $cmd .= " restart " . escapeshellarg($servicename); 56 | 57 | return $this->RunCommand($cmd, $display); 58 | } 59 | 60 | public function Reload($servicename, $display = false) 61 | { 62 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 63 | $cmd .= " reload " . escapeshellarg($servicename); 64 | 65 | return $this->RunCommand($cmd, $display); 66 | } 67 | 68 | public function WaitFor($servicename, $display = false) 69 | { 70 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 71 | $cmd .= " waitfor " . escapeshellarg($servicename); 72 | 73 | return $this->RunCommand($cmd, $display); 74 | } 75 | 76 | public function Status($servicename, $display = false) 77 | { 78 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 79 | $cmd .= " status " . escapeshellarg($servicename); 80 | 81 | return $this->RunCommand($cmd, $display); 82 | } 83 | 84 | public function GetConfig($servicename) 85 | { 86 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 87 | $cmd .= " configfile " . escapeshellarg($servicename); 88 | 89 | $result = $this->RunCommand($cmd); 90 | if (!$result["success"]) return $result; 91 | 92 | $filename = trim($result["output"]); 93 | if (strtolower(substr($filename, 0, 6)) === "error:") return array("success" => false, "error" => self::SMTranslate("Unable to locate the '%s' configuration file.", $servicename), "errorcode" => "missing_config", "info" => $result); 94 | 95 | $fp = fopen($filename, "rb"); 96 | if ($fp === false) return array("success" => false, "error" => self::SMTranslate("Unable to open the configuration file '%s' for reading.", $filename), "errorcode" => "fopen_failed"); 97 | 98 | $result = array( 99 | "success" => true, 100 | "filename" => $filename, 101 | "options" => array() 102 | ); 103 | 104 | while (($line = fgets($fp)) !== false) 105 | { 106 | $line = trim($line); 107 | 108 | $pos = strpos($line, "="); 109 | if ($pos !== false) 110 | { 111 | $key = substr($line, 0, $pos); 112 | $val = (string)substr($line, $pos + 1); 113 | 114 | if (!isset($result["options"][$key])) $result["options"][$key] = $val; 115 | } 116 | 117 | if (feof($fp)) break; 118 | } 119 | 120 | fclose($fp); 121 | 122 | return $result; 123 | } 124 | 125 | public function AddAction($servicename, $actionname, $actiondesc, $phpfile, $args, $display = false) 126 | { 127 | $cmd = escapeshellarg($this->GetServiceManagerRealpath()); 128 | $cmd .= " addaction " . escapeshellarg($servicename); 129 | $cmd .= " " . escapeshellarg($actionname); 130 | $cmd .= " " . escapeshellarg($actiondesc); 131 | $cmd .= " " . escapeshellarg($this->GetPHPBinary()); 132 | $cmd .= " " . escapeshellarg($phpfile); 133 | foreach ($args as $arg) $cmd .= " " . escapeshellarg($arg); 134 | 135 | return $this->RunCommand($cmd, $display); 136 | } 137 | 138 | public function GetServiceManagerRealpath() 139 | { 140 | $os = php_uname("s"); 141 | 142 | if (strtoupper(substr($os, 0, 3)) == "WIN") $result = $this->rootpath . "\\servicemanager.exe"; 143 | else 144 | { 145 | if (file_exists($this->rootpath . "/servicemanager_nix")) $result = $this->rootpath . "/servicemanager_nix"; 146 | else if (file_exists("/usr/local/bin/servicemanager")) $result = "/usr/local/bin/servicemanager"; 147 | else if (strtoupper(substr($os, 0, 6)) == "DARWIN") $result = $this->rootpath . "/servicemanager_mac"; 148 | else if (PHP_INT_SIZE >= 8) $result = $this->rootpath . "/servicemanager_nix_64"; 149 | else $result = $this->rootpath . "/servicemanager_nix_32"; 150 | 151 | @chmod($result, 0755); 152 | } 153 | 154 | return $result; 155 | } 156 | 157 | public function GetPHPBinary() 158 | { 159 | if (file_exists("/usr/bin/php") && realpath("/usr/bin/php") === PHP_BINARY) $result = "/usr/bin/php"; 160 | else $result = PHP_BINARY; 161 | 162 | return $result; 163 | } 164 | 165 | public function RunCommand($cmd, $display = false) 166 | { 167 | if ($display) echo self::SMTranslate("Running command: %s", $cmd) . "\n\n"; 168 | 169 | $fp = popen($cmd, "rb"); 170 | if ($fp === false) return array("success" => false, "error" => self::SMTranslate("The executable failed to start."), "errorcode" => "start_process", "info" => $cmd); 171 | 172 | $data = ""; 173 | while (($data2 = fread($fp, 65536)) !== false) 174 | { 175 | if ($display) echo $data2; 176 | $data .= $data2; 177 | 178 | if (feof($fp)) break; 179 | } 180 | 181 | pclose($fp); 182 | 183 | if ($display) echo "\n"; 184 | 185 | return array("success" => true, "cmd" => $cmd, "output" => $data); 186 | } 187 | 188 | protected static function SMTranslate() 189 | { 190 | $args = func_get_args(); 191 | if (!count($args)) return ""; 192 | 193 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 194 | } 195 | } 196 | ?> -------------------------------------------------------------------------------- /support/remotedapi_web_server.php: -------------------------------------------------------------------------------- 1 | rws = false; 17 | $this->rhost = false; 18 | $this->rwr = false; 19 | $this->rwrclients = array(); 20 | $this->rwrnextclientid = 1; 21 | } 22 | 23 | // Helper function to decide which class to use to handle the server. 24 | public static function IsRemoted($host) 25 | { 26 | return (strtolower(substr($host, 0, 6)) === "rws://" || strtolower(substr($host, 0, 7)) === "rwss://"); 27 | } 28 | 29 | // Reconnects to the WebSocket. 30 | // This is a blocking call. In theory, that shouldn't cause too many problems. 31 | private function ReconnectRemotedAPIWebSocket() 32 | { 33 | if (!class_exists("HTTP", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/http.php"; 34 | if (!class_exists("WebRoute", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/webroute.php"; 35 | 36 | $url = $this->rhost; 37 | if ($url["scheme"] !== "rws" && $url["scheme"] !== "rwss") return array("success" => false, "error" => HTTP::HTTPTranslate("Invalid Remoted API URL scheme."), "errorcode" => "invalid_scheme"); 38 | if ($url["loginusername"] === "") return array("success" => false, "error" => HTTP::HTTPTranslate("Remoted API URL is missing server key."), "errorcode" => "missing_server_key"); 39 | 40 | $options = array("headers" => array()); 41 | $options["headers"]["X-Remoted-APIKey"] = $url["loginusername"]; 42 | 43 | $url["scheme"] = ($url["scheme"] === "rws" ? "ws" : "wss"); 44 | unset($url["loginusername"]); 45 | unset($url["login"]); 46 | 47 | $url2 = "http://" . $url["host"]; 48 | 49 | $url = HTTP::CondenseURL($url); 50 | 51 | return $this->rws->Connect($url, $url2, $options); 52 | } 53 | 54 | // Overrides the default behavior to start a server on a given host and port. 55 | // $host expected URL format: rws://serverapikey@host/webroutepath 56 | // Where 'serverapikey' is the Remoted API Server server API key and 'webroutepath' is the unique path to connect under. 57 | public function Start($host, $port, $sslopts = false) 58 | { 59 | $this->Stop(); 60 | 61 | if (!class_exists("WebSocket", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/websocket.php"; 62 | if (!class_exists("WebRoute", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/webroute.php"; 63 | 64 | $this->rws = new WebSocket(); 65 | $this->rhost = HTTP::ExtractURL($host); 66 | 67 | $this->rwr = new WebRoute(); 68 | 69 | return $this->ReconnectRemotedAPIWebSocket(); 70 | } 71 | 72 | public function Stop() 73 | { 74 | parent::Stop(); 75 | 76 | if ($this->rws !== false) $this->rws->Disconnect(); 77 | 78 | foreach ($this->rwrclients as $id => $client) 79 | { 80 | if ($client["state"]->webstate !== false && $client["state"]->webstate["httpstate"] !== false) 81 | { 82 | HTTP::ForceClose($client["state"]->webstate["httpstate"]); 83 | } 84 | } 85 | 86 | $this->rwrclients = array(); 87 | } 88 | 89 | // Adds the WebSocket server stream and WebRoute clients. 90 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 91 | { 92 | if ($this->rws !== false) 93 | { 94 | if ($this->rws->GetStream() === false) $this->ReconnectRemotedAPIWebSocket(); 95 | 96 | if ($this->rws->GetStream() === false) $timeout = ($timeout === false || $timeout > 1 ? 1 : 0); 97 | else 98 | { 99 | $readfps[$prefix . "rws_http_s"] = $this->rws->GetStream(); 100 | 101 | if ($this->rws->NeedsWrite()) $writefps[$prefix . "rws_http_s"] = $this->rws->GetStream(); 102 | } 103 | } 104 | 105 | foreach ($this->rwrclients as $id => $client) 106 | { 107 | if ($client["state"]->webstate !== false && $client["state"]->webstate["httpstate"] !== false) 108 | { 109 | if (HTTP::WantRead($client["state"]->webstate["httpstate"])) $readfps[$prefix . "rws_http_c_" . $id] = $client["state"]->webstate["httpstate"]["fp"]; 110 | if (HTTP::WantWrite($client["state"]->webstate["httpstate"])) $writefps[$prefix . "rws_http_c_" . $id] = $client["state"]->webstate["httpstate"]["fp"]; 111 | } 112 | } 113 | 114 | parent::UpdateStreamsAndTimeout($prefix, $timeout, $readfps, $writefps); 115 | } 116 | 117 | protected function HandleNewConnections(&$readfps, &$writefps) 118 | { 119 | $result = $this->rws->ProcessQueuesAndTimeoutState(isset($readfps["rws_http_s"]), isset($writefps["rws_http_s"])); 120 | if ($result["success"]) 121 | { 122 | // Initiate a new async WebRoute for each incoming connection request. 123 | $result = $this->rws->Read(); 124 | while ($result["success"] && $result["data"] !== false) 125 | { 126 | $data = json_decode($result["data"]["payload"], true); 127 | 128 | if (isset($data["ipaddr"]) && isset($data["id"]) && isset($data["timeout"])) 129 | { 130 | $url = $this->rhost; 131 | 132 | $options = array( 133 | "async" => true, 134 | "headers" => array() 135 | ); 136 | 137 | $options["headers"]["X-Remoted-APIKey"] = $url["loginusername"]; 138 | 139 | $url["scheme"] = ($url["scheme"] === "rws" ? "wr" : "wrs"); 140 | unset($url["loginusername"]); 141 | unset($url["login"]); 142 | 143 | $data["url"] = HTTP::CondenseURL($url); 144 | $data["retries"] = 3; 145 | $data["options"] = $options; 146 | 147 | // Due to the async setting, this will only initiate the connection. No data is actually sent/received at this point. 148 | $result = $this->rwr->Connect($data["url"], $data["id"], $data["timeout"], $data["options"]); 149 | if ($result["success"]) 150 | { 151 | $result["data"] = $data; 152 | 153 | $this->rwrclients[$this->rwrnextclientid] = $result; 154 | 155 | $this->rwrnextclientid++; 156 | } 157 | } 158 | 159 | $result = $this->rws->Read(); 160 | } 161 | } 162 | 163 | // WebSocket was disconnected due to a socket error. 164 | if (!$result["success"]) $this->rws->Disconnect(); 165 | 166 | unset($readfps["rws_http_s"]); 167 | unset($writefps["rws_http_s"]); 168 | 169 | // Handle WebRoute clients. 170 | foreach ($this->rwrclients as $id => $client) 171 | { 172 | $result = $this->rwr->ProcessState($client["state"]); 173 | if ($result["success"]) 174 | { 175 | // WebRoute successfully established. Convert it into a WebServer client. 176 | // At this point, the underlying WebServer class takes over. 177 | $client2 = $this->InitNewClient(); 178 | $client2->fp = $result["fp"]; 179 | $client2->ipaddr = $client["data"]["ipaddr"]; 180 | 181 | // Remove the WebRoute client state. 182 | unset($this->rwrclients[$id]); 183 | } 184 | else if ($result["errorcode"] !== "no_data") 185 | { 186 | // Retry a few times in case of a flaky connection. 187 | $data = $client["data"]; 188 | if ($data["retries"] > 0) 189 | { 190 | $data["retries"]--; 191 | 192 | $result = $this->rwr->Connect($data["url"], $data["id"], $data["timeout"], $data["options"]); 193 | if ($result["success"]) 194 | { 195 | $result["data"] = $data; 196 | 197 | $this->rwrclients[$id] = $result; 198 | } 199 | } 200 | else 201 | { 202 | // Client connection failed. 203 | unset($this->rwrclients[$id]); 204 | } 205 | } 206 | } 207 | } 208 | } 209 | ?> -------------------------------------------------------------------------------- /support/multi_async_helper.php: -------------------------------------------------------------------------------- 1 | objs = array(); 12 | $this->queuedobjs = array(); 13 | $this->limit = false; 14 | } 15 | 16 | public function SetConcurrencyLimit($limit) 17 | { 18 | $this->limit = $limit; 19 | } 20 | 21 | public function Set($key, $obj, $callback) 22 | { 23 | if (is_callable($callback)) 24 | { 25 | $this->queuedobjs[$key] = array( 26 | "obj" => $obj, 27 | "callback" => $callback 28 | ); 29 | 30 | unset($this->objs[$key]); 31 | } 32 | } 33 | 34 | public function NumObjects() 35 | { 36 | return count($this->queuedobjs) + count($this->objs); 37 | } 38 | 39 | public function GetObject($key) 40 | { 41 | if (isset($this->queuedobjs[$key])) $result = $this->queuedobjs[$key]["obj"]; 42 | else if (isset($this->objs[$key])) $result = $this->objs[$key]["obj"]; 43 | else $result = false; 44 | 45 | return $result; 46 | } 47 | 48 | // To be able to change a callback on the fly. 49 | public function SetCallback($key, $callback) 50 | { 51 | if (is_callable($callback)) 52 | { 53 | if (isset($this->queuedobjs[$key])) $this->queuedobjs[$key]["callback"] = $callback; 54 | else if (isset($this->objs[$key])) $this->objs[$key]["callback"] = $callback; 55 | } 56 | } 57 | 58 | private function InternalDetach($key, $cleanup) 59 | { 60 | if (isset($this->queuedobjs[$key])) 61 | { 62 | call_user_func_array($this->queuedobjs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->queuedobjs[$key]["obj"])); 63 | $result = $this->queuedobjs[$key]["obj"]; 64 | unset($this->queuedobjs[$key]); 65 | } 66 | else if (isset($this->objs[$key])) 67 | { 68 | call_user_func_array($this->objs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->objs[$key]["obj"])); 69 | $result = $this->objs[$key]["obj"]; 70 | unset($this->objs[$key]); 71 | } 72 | else 73 | { 74 | $result = false; 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | public function Detach($key) 81 | { 82 | return $this->InternalDetach($key, false); 83 | } 84 | 85 | public function Remove($key) 86 | { 87 | return $this->InternalDetach($key, true); 88 | } 89 | 90 | // A few default functions for direct file/socket handles. 91 | public static function ReadOnly($mode, &$data, $key, $fp) 92 | { 93 | switch ($mode) 94 | { 95 | case "init": 96 | case "update": 97 | { 98 | // Move to/Keep in the live queue. 99 | if (is_resource($fp)) $data = true; 100 | 101 | break; 102 | } 103 | case "read": 104 | case "write": 105 | case "writefps": 106 | { 107 | break; 108 | } 109 | case "readfps": 110 | { 111 | $data[$key] = $fp; 112 | 113 | break; 114 | } 115 | case "cleanup": 116 | { 117 | if ($data === true) @fclose($fp); 118 | 119 | break; 120 | } 121 | } 122 | } 123 | 124 | public static function WriteOnly($mode, &$data, $key, $fp) 125 | { 126 | switch ($mode) 127 | { 128 | case "init": 129 | case "update": 130 | { 131 | // Move to/Keep in the live queue. 132 | if (is_resource($fp)) $data = true; 133 | 134 | break; 135 | } 136 | case "read": 137 | case "readfps": 138 | case "write": 139 | { 140 | break; 141 | } 142 | case "writefps": 143 | { 144 | $data[$key] = $fp; 145 | 146 | break; 147 | } 148 | case "cleanup": 149 | { 150 | if ($data === true) @fclose($fp); 151 | 152 | break; 153 | } 154 | } 155 | } 156 | 157 | public static function ReadAndWrite($mode, &$data, $key, $fp) 158 | { 159 | switch ($mode) 160 | { 161 | case "init": 162 | case "update": 163 | { 164 | // Move to/Keep in the live queue. 165 | if (is_resource($fp)) $data = true; 166 | 167 | break; 168 | } 169 | case "read": 170 | case "write": 171 | { 172 | break; 173 | } 174 | case "readfps": 175 | case "writefps": 176 | { 177 | $data[$key] = $fp; 178 | 179 | break; 180 | } 181 | case "cleanup": 182 | { 183 | if ($data === true) @fclose($fp); 184 | 185 | break; 186 | } 187 | } 188 | } 189 | 190 | public function Wait($timeout = false) 191 | { 192 | // Move queued objects to live. 193 | $result2 = array("success" => true, "read" => array(), "write" => array(), "removed" => array(), "new" => array()); 194 | while (count($this->queuedobjs) && ($this->limit === false || count($this->objs) < $this->limit)) 195 | { 196 | $info = reset($this->queuedobjs); 197 | $key = key($this->queuedobjs); 198 | unset($this->queuedobjs[$key]); 199 | 200 | $result2["new"][$key] = $key; 201 | 202 | $keep = false; 203 | call_user_func_array($info["callback"], array("init", &$keep, $key, &$info["obj"])); 204 | 205 | $this->objs[$key] = $info; 206 | 207 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 208 | } 209 | 210 | // Walk the objects looking for read and write handles. 211 | $readfps = array(); 212 | $writefps = array(); 213 | $exceptfps = NULL; 214 | foreach ($this->objs as $key => &$info) 215 | { 216 | $keep = false; 217 | call_user_func_array($info["callback"], array("update", &$keep, $key, &$info["obj"])); 218 | 219 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 220 | else 221 | { 222 | call_user_func_array($info["callback"], array("readfps", &$readfps, $key, &$info["obj"])); 223 | call_user_func_array($info["callback"], array("writefps", &$writefps, $key, &$info["obj"])); 224 | } 225 | } 226 | if (!count($readfps)) $readfps = NULL; 227 | if (!count($writefps)) $writefps = NULL; 228 | 229 | // Wait for something to happen. 230 | if (isset($readfps) || isset($writefps)) 231 | { 232 | if ($timeout === false) $timeout = NULL; 233 | $readfps2 = $readfps; 234 | $writefps2 = $writefps; 235 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 236 | if ($result === false) return array("success" => false, "error" => self::MAHTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 237 | else if ($result > 0) 238 | { 239 | if (isset($readfps)) 240 | { 241 | $readfps3 = array(); 242 | foreach ($readfps as $key => $fp) 243 | { 244 | if (!isset($readfps2[$key]) || $readfps2[$key] !== $fp) 245 | { 246 | foreach ($readfps2 as $key2 => $fp2) 247 | { 248 | if ($fp === $fp2) $key = $key2; 249 | } 250 | } 251 | 252 | if (isset($this->objs[$key])) 253 | { 254 | call_user_func_array($this->objs[$key]["callback"], array("read", &$fp, $key, &$this->objs[$key]["obj"])); 255 | 256 | $readfps3[$key] = $fp; 257 | } 258 | } 259 | 260 | $result2["read"] = $readfps3; 261 | } 262 | 263 | if (isset($writefps)) 264 | { 265 | $writefps3 = array(); 266 | foreach ($writefps as $key => $fp) 267 | { 268 | if (!isset($writefps2[$key]) || $writefps2[$key] !== $fp) 269 | { 270 | foreach ($writefps2 as $key2 => $fp2) 271 | { 272 | if ($fp === $fp2) $key = $key2; 273 | } 274 | } 275 | 276 | if (isset($this->objs[$key])) 277 | { 278 | call_user_func_array($this->objs[$key]["callback"], array("write", &$fp, $key, &$this->objs[$key]["obj"])); 279 | 280 | $readfps3[$key] = $fp; 281 | } 282 | } 283 | 284 | $result2["write"] = $writefps3; 285 | } 286 | } 287 | } 288 | 289 | $result2["numleft"] = count($this->queuedobjs) + count($this->objs); 290 | 291 | return $result2; 292 | } 293 | 294 | public static function MAHTranslate() 295 | { 296 | $args = func_get_args(); 297 | if (!count($args)) return ""; 298 | 299 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 300 | } 301 | } 302 | ?> -------------------------------------------------------------------------------- /support/webroute.php: -------------------------------------------------------------------------------- 1 | csprng = false; 17 | } 18 | 19 | public static function ProcessState($state) 20 | { 21 | while ($state->state !== "done") 22 | { 23 | switch ($state->state) 24 | { 25 | case "initialize": 26 | { 27 | $result = $state->web->Process($state->url, $state->options); 28 | if (!$result["success"]) return $result; 29 | 30 | if (isset($state->options["async"]) && $state->options["async"]) 31 | { 32 | $state->async = true; 33 | $state->webstate = $result["state"]; 34 | 35 | $state->state = "process_async"; 36 | } 37 | else 38 | { 39 | $state->result = $result; 40 | 41 | $state->state = "post_retrieval"; 42 | } 43 | 44 | break; 45 | } 46 | case "process_async": 47 | { 48 | // Run a cycle of the WebBrowser state processor. 49 | $result = $state->web->ProcessState($state->webstate); 50 | if (!$result["success"]) return $result; 51 | 52 | $state->webstate = false; 53 | $state->result = $result; 54 | 55 | $state->state = "post_retrieval"; 56 | 57 | break; 58 | } 59 | case "post_retrieval": 60 | { 61 | if ($state->result["response"]["code"] != 101) return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() failed to connect to the WebRoute. Server returned: %s %s", $result["response"]["code"], $result["response"]["meaning"]), "errorcode" => "incorrect_server_response"); 62 | if (!isset($state->result["headers"]["Sec-Webroute-Accept"])) return array("success" => false, "error" => self::WRTranslate("Server failed to include a 'Sec-WebRoute-Accept' header in its response to the request."), "errorcode" => "missing_server_webroute_accept_header"); 63 | 64 | // Verify the Sec-WebRoute-Accept response. 65 | if ($state->result["headers"]["Sec-Webroute-Accept"][0] !== base64_encode(sha1($state->options["headers"]["WebRoute-ID"] . self::ID_GUID, true))) return array("success" => false, "error" => self::WRTranslate("The server's 'Sec-WebRoute-Accept' header is invalid."), "errorcode" => "invalid_server_webroute_accept_header"); 66 | 67 | $state->state = "done"; 68 | 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return $state->result; 75 | } 76 | 77 | public function Connect($url, $id = false, $timeout = false, $options = array(), $web = false) 78 | { 79 | // Generate client ID. 80 | if ($id === false) 81 | { 82 | if (!class_exists("CSPRNG", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/random.php"; 83 | 84 | if ($this->csprng === false) $this->csprng = new CSPRNG(); 85 | 86 | $id = $this->csprng->GenerateString(64); 87 | } 88 | 89 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 90 | 91 | // Use WebBrowser to initiate the connection. 92 | if ($web === false) $web = new WebBrowser(); 93 | 94 | // Transform URL. 95 | $url2 = HTTP::ExtractURL($url); 96 | if ($url2["scheme"] != "wr" && $url2["scheme"] != "wrs") return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() only supports the 'wr' and 'wrs' protocols."), "errorcode" => "protocol_check"); 97 | $url2["scheme"] = str_replace("wr", "http", $url2["scheme"]); 98 | $url2 = HTTP::CondenseURL($url2); 99 | 100 | // Generate correct request headers. 101 | if (!isset($options["headers"])) $options["headers"] = array(); 102 | $options["headers"]["Connection"] = "keep-alive, Upgrade"; 103 | $options["headers"]["Pragma"] = "no-cache"; 104 | $options["headers"]["WebRoute-Version"] = "1"; 105 | $options["headers"]["WebRoute-ID"] = $id; 106 | if ($timeout !== false && is_int($timeout)) $options["headers"]["WebRoute-Timeout"] = (string)(int)$timeout; 107 | $options["headers"]["Upgrade"] = "webroute"; 108 | 109 | // Initialize the process state object. 110 | $state = new stdClass(); 111 | $state->async = false; 112 | $state->state = "initialize"; 113 | $state->web = $web; 114 | $state->url = $url2; 115 | $state->options = $options; 116 | $state->webstate = false; 117 | $state->result = false; 118 | 119 | // Run at least one state cycle to finish initializing the state object. 120 | $result = $this->ProcessState($state); 121 | 122 | // Return the state for async calls. Caller must call ProcessState(). 123 | if ($state->async) return array("success" => true, "id" => $id, "state" => $state); 124 | 125 | $result["id"] = $id; 126 | 127 | return $result; 128 | } 129 | 130 | // Implements the correct MultiAsyncHelper responses for WebRoute instances. 131 | public function ConnectAsync__Handler($mode, &$data, $key, $info) 132 | { 133 | switch ($mode) 134 | { 135 | case "init": 136 | { 137 | if ($info->init) $data = $info->keep; 138 | else 139 | { 140 | $info->result = $this->Connect($info->url, $info->id, $info->timeout, $info->options, $info->web); 141 | if (!$info->result["success"]) 142 | { 143 | $info->keep = false; 144 | 145 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 146 | } 147 | else 148 | { 149 | $info->id = $info->result["id"]; 150 | $info->state = $info->result["state"]; 151 | 152 | // Move to the live queue. 153 | $data = true; 154 | } 155 | } 156 | 157 | break; 158 | } 159 | case "update": 160 | case "read": 161 | case "write": 162 | { 163 | if ($info->keep) 164 | { 165 | $info->result = $this->ProcessState($info->state); 166 | if ($info->result["success"] || $info->result["errorcode"] !== "no_data") $info->keep = false; 167 | 168 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 169 | 170 | if ($mode === "update") $data = $info->keep; 171 | } 172 | 173 | break; 174 | } 175 | case "readfps": 176 | { 177 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantRead($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 178 | 179 | break; 180 | } 181 | case "writefps": 182 | { 183 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantWrite($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 184 | 185 | break; 186 | } 187 | case "cleanup": 188 | { 189 | // When true, caller is removing. Otherwise, detaching from the queue. 190 | if ($data === true) 191 | { 192 | if (isset($info->state)) 193 | { 194 | if ($info->state->webstate["httpstate"] !== false) HTTP::ForceClose($info->state->webstate["httpstate"]); 195 | 196 | unset($info->state); 197 | } 198 | 199 | $info->keep = false; 200 | } 201 | 202 | break; 203 | } 204 | } 205 | } 206 | 207 | public function ConnectAsync($helper, $key, $callback, $url, $id = false, $timeout = false, $options = array(), $web = false) 208 | { 209 | $options["async"] = true; 210 | 211 | $info = new stdClass(); 212 | $info->init = false; 213 | $info->keep = true; 214 | $info->callback = $callback; 215 | $info->url = $url; 216 | $info->id = $id; 217 | $info->timeout = $timeout; 218 | $info->options = $options; 219 | $info->web = $web; 220 | $info->result = false; 221 | 222 | $helper->Set($key, $info, array($this, "ConnectAsync__Handler")); 223 | 224 | return array("success" => true); 225 | } 226 | 227 | public static function WRTranslate() 228 | { 229 | $args = func_get_args(); 230 | if (!count($args)) return ""; 231 | 232 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 233 | } 234 | } 235 | ?> -------------------------------------------------------------------------------- /sdks/php/webroute.php: -------------------------------------------------------------------------------- 1 | csprng = false; 17 | } 18 | 19 | public static function ProcessState($state) 20 | { 21 | while ($state->state !== "done") 22 | { 23 | switch ($state->state) 24 | { 25 | case "initialize": 26 | { 27 | $result = $state->web->Process($state->url, $state->options); 28 | if (!$result["success"]) return $result; 29 | 30 | if (isset($state->options["async"]) && $state->options["async"]) 31 | { 32 | $state->async = true; 33 | $state->webstate = $result["state"]; 34 | 35 | $state->state = "process_async"; 36 | } 37 | else 38 | { 39 | $state->result = $result; 40 | 41 | $state->state = "post_retrieval"; 42 | } 43 | 44 | break; 45 | } 46 | case "process_async": 47 | { 48 | // Run a cycle of the WebBrowser state processor. 49 | $result = $state->web->ProcessState($state->webstate); 50 | if (!$result["success"]) return $result; 51 | 52 | $state->webstate = false; 53 | $state->result = $result; 54 | 55 | $state->state = "post_retrieval"; 56 | 57 | break; 58 | } 59 | case "post_retrieval": 60 | { 61 | if ($state->result["response"]["code"] != 101) return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() failed to connect to the WebRoute. Server returned: %s %s", $result["response"]["code"], $result["response"]["meaning"]), "errorcode" => "incorrect_server_response"); 62 | if (!isset($state->result["headers"]["Sec-Webroute-Accept"])) return array("success" => false, "error" => self::WRTranslate("Server failed to include a 'Sec-WebRoute-Accept' header in its response to the request."), "errorcode" => "missing_server_webroute_accept_header"); 63 | 64 | // Verify the Sec-WebRoute-Accept response. 65 | if ($state->result["headers"]["Sec-Webroute-Accept"][0] !== base64_encode(sha1($state->options["headers"]["WebRoute-ID"] . self::ID_GUID, true))) return array("success" => false, "error" => self::WRTranslate("The server's 'Sec-WebRoute-Accept' header is invalid."), "errorcode" => "invalid_server_webroute_accept_header"); 66 | 67 | $state->state = "done"; 68 | 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return $state->result; 75 | } 76 | 77 | public function Connect($url, $id = false, $timeout = false, $options = array(), $web = false) 78 | { 79 | // Generate client ID. 80 | if ($id === false) 81 | { 82 | if (!class_exists("CSPRNG", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/random.php"; 83 | 84 | if ($this->csprng === false) $this->csprng = new CSPRNG(); 85 | 86 | $id = $this->csprng->GenerateString(64); 87 | } 88 | 89 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 90 | 91 | // Use WebBrowser to initiate the connection. 92 | if ($web === false) $web = new WebBrowser(); 93 | 94 | // Transform URL. 95 | $url2 = HTTP::ExtractURL($url); 96 | if ($url2["scheme"] != "wr" && $url2["scheme"] != "wrs") return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() only supports the 'wr' and 'wrs' protocols."), "errorcode" => "protocol_check"); 97 | $url2["scheme"] = str_replace("wr", "http", $url2["scheme"]); 98 | $url2 = HTTP::CondenseURL($url2); 99 | 100 | // Generate correct request headers. 101 | if (!isset($options["headers"])) $options["headers"] = array(); 102 | $options["headers"]["Connection"] = "keep-alive, Upgrade"; 103 | $options["headers"]["Pragma"] = "no-cache"; 104 | $options["headers"]["WebRoute-Version"] = "1"; 105 | $options["headers"]["WebRoute-ID"] = $id; 106 | if ($timeout !== false && is_int($timeout)) $options["headers"]["WebRoute-Timeout"] = (string)(int)$timeout; 107 | $options["headers"]["Upgrade"] = "webroute"; 108 | 109 | // Initialize the process state object. 110 | $state = new stdClass(); 111 | $state->async = false; 112 | $state->state = "initialize"; 113 | $state->web = $web; 114 | $state->url = $url2; 115 | $state->options = $options; 116 | $state->webstate = false; 117 | $state->result = false; 118 | 119 | // Run at least one state cycle to finish initializing the state object. 120 | $result = $this->ProcessState($state); 121 | 122 | // Return the state for async calls. Caller must call ProcessState(). 123 | if ($state->async) return array("success" => true, "id" => $id, "state" => $state); 124 | 125 | $result["id"] = $id; 126 | 127 | return $result; 128 | } 129 | 130 | // Implements the correct MultiAsyncHelper responses for WebRoute instances. 131 | public function ConnectAsync__Handler($mode, &$data, $key, $info) 132 | { 133 | switch ($mode) 134 | { 135 | case "init": 136 | { 137 | if ($info->init) $data = $info->keep; 138 | else 139 | { 140 | $info->result = $this->Connect($info->url, $info->id, $info->timeout, $info->options, $info->web); 141 | if (!$info->result["success"]) 142 | { 143 | $info->keep = false; 144 | 145 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 146 | } 147 | else 148 | { 149 | $info->id = $info->result["id"]; 150 | $info->state = $info->result["state"]; 151 | 152 | // Move to the live queue. 153 | $data = true; 154 | } 155 | } 156 | 157 | break; 158 | } 159 | case "update": 160 | case "read": 161 | case "write": 162 | { 163 | if ($info->keep) 164 | { 165 | $info->result = $this->ProcessState($info->state); 166 | if ($info->result["success"] || $info->result["errorcode"] !== "no_data") $info->keep = false; 167 | 168 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 169 | 170 | if ($mode === "update") $data = $info->keep; 171 | } 172 | 173 | break; 174 | } 175 | case "readfps": 176 | { 177 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantRead($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 178 | 179 | break; 180 | } 181 | case "writefps": 182 | { 183 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantWrite($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 184 | 185 | break; 186 | } 187 | case "cleanup": 188 | { 189 | // When true, caller is removing. Otherwise, detaching from the queue. 190 | if ($data === true) 191 | { 192 | if (isset($info->state)) 193 | { 194 | if ($info->state->webstate["httpstate"] !== false) HTTP::ForceClose($info->state->webstate["httpstate"]); 195 | 196 | unset($info->state); 197 | } 198 | 199 | $info->keep = false; 200 | } 201 | 202 | break; 203 | } 204 | } 205 | } 206 | 207 | public function ConnectAsync($helper, $key, $callback, $url, $id = false, $timeout = false, $options = array(), $web = false) 208 | { 209 | $options["async"] = true; 210 | 211 | $info = new stdClass(); 212 | $info->init = false; 213 | $info->keep = true; 214 | $info->callback = $callback; 215 | $info->url = $url; 216 | $info->id = $id; 217 | $info->timeout = $timeout; 218 | $info->options = $options; 219 | $info->web = $web; 220 | $info->result = false; 221 | 222 | $helper->Set($key, $info, array($this, "ConnectAsync__Handler")); 223 | 224 | return array("success" => true); 225 | } 226 | 227 | public static function WRTranslate() 228 | { 229 | $args = func_get_args(); 230 | if (!count($args)) return ""; 231 | 232 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 233 | } 234 | } 235 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Remoted API Server and SDK 2 | ========================== 3 | 4 | Allows any standard TCP/IP server to be remoted with low-overhead TCP connectivity. Allows TCP/IP clients to easily and directly connect to a TCP/IP server operating completely behind a firewall by utilizing the [WebRoute protocol](https://github.com/cubiclesoft/webroute). 5 | 6 | The Remoted API Server and SDK turn client/server architecture upside down and, in the process, completely bypasses most standard firewall policies. 7 | 8 | [![Donate](https://cubiclesoft.com/res/donate-shield.png)](https://cubiclesoft.com/donate/) [![Discord](https://img.shields.io/discord/777282089980526602?label=chat&logo=discord)](https://cubiclesoft.com/product-support/github/) 9 | 10 | [Original blog post](https://cubicspot.blogspot.com/2017/09/i-um-broke-stuff.html) 11 | 12 | Features 13 | -------- 14 | 15 | * A simple system installer. Includes [Service Manager](https://github.com/cubiclesoft/service-manager) for at-boot startup. 16 | * An easy one-line override `RemotedAPIWebServer` class for anyone already using the CubicleSoft [WebServer](https://github.com/cubiclesoft/ultimate-web-scraper/blob/master/web_server.php) class to build a server. 17 | * A SDK for providing quick connections to Remoted API servers. 18 | * Bypasses most firewall configurations with ease while simultaneously not sacrificing system security. 19 | * Also has a liberal open source license. MIT or LGPL, your choice. 20 | * Designed for relatively painless integration into your environment. 21 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 22 | 23 | Installation 24 | ------------ 25 | 26 | Installing the Remoted API Server is done from the command line as a system administrator (e.g. root). Once there, run: 27 | 28 | `php install.php` 29 | 30 | Which will provide a guided install. The defaults are usually good enough. The installer can be re-run at any time (e.g. when upgrading). To skip installing the server as a system service, enter a singly hyphen '-' when the installer asks. 31 | 32 | After installing the server, if it isn't running as a system service or you want to run it manually, run: 33 | 34 | `php server.php` 35 | 36 | Once the server is running, hook it up to a standard web server as a reverse proxy. Instructions for how to do this part can be found for most web servers including [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html) and [Nginx](https://www.nginx.com/resources/admin-guide/reverse-proxy/). If you've set up WebSocket servers before (e.g. NodeJS), then the procedure is identical. 37 | 38 | Testing 39 | ------- 40 | 41 | The software includes a test suite that sets up an example server that attaches to the already started Remoted API Server and provides a custom protocol and a client that connects to the server. To run this test and verify functionality, follow these simple steps using separate terminal/console/command line windows: 42 | 43 | * Start the server by running `tests\test_server.php`. This establishes a WebSocket connection with the Remoted API Server instead of a traditional bind()/accept(). 44 | * Start the client by running `tests\test_client.php`. This establishes a WebRoute connection with the Remoted API Server and retrieves data sent by the server. 45 | 46 | These are demos upon which other software can be based. You can use the source code to both the test server and test client as a starting template for your own integrations. 47 | 48 | For additional example integrations in real products, see [Cloud Storage Server](https://github.com/cubiclesoft/cloud-storage-server) and [Cloud Backup](https://github.com/cubiclesoft/cloud-backup). 49 | 50 | How It Works 51 | ------------ 52 | 53 | The installer establishes two security tokens: A server API key and a client API key. These two tokens should be kept secure as they allow clients to connect and act either as a server or a client depending on which key is used. The appropriate key is sent as the custom HTTP header `X-Remoted-APIKey`. 54 | 55 | A standard TCP/IP server normally makes a call to bind() on a specific port on the local machine and later calls accept() to accept incoming connections. A Remoted API Server enabled server will, instead, establish a WebSocket connection using the aforementioned server API key with the Remoted API Server and specify the path that it is listening on. At this point, the server is "waiting" for connections. It is possible, in many cases only a few adjustments are needed, to transparently replace the bind()/accept() calls with a derived class in whatever language is being used. 56 | 57 | Once the WebSocket has been established, a client can connect into the Remoted API Server using the client API key and the same path as the server over the WebRoute protocol. If the server isn't connected, the client will receive a 502 Bad Gateway response. 58 | 59 | Upon receiving a new client upon a path that exists, the server is notified of the connection via the established WebSocket. Of importance, the WebRoute ID is passed along to the server. Once the server processes the packet of data, it too now establishes a WebRoute connection (i.e. a brand new TCP/IP connection) with the Remoted API Server. 60 | 61 | Once the Remoted API Server links the client and server TCP/IP connections together as per the WebRoute protocol, it lets the data flow in both directions unhindered. On the server side of things, the new TCP/IP socket is treated as if it had just been returned from an accept() call. 62 | 63 | Use Cases 64 | --------- 65 | 66 | Let's say you want to move some data to a server behind a firewall on an automated basis (e.g. in response to some user action). However, you don't want to or can't open any ports on the firewall for whatever reason. Without Remoted API Server, you would have to store that data temporarily outside the firewall and then periodically pick it up using a process running behind the firewall. There are also complications with making sure that all data is written out to disk before picking up the file as well as making sure that the file is removed/archived in a timely manner once it has been picked up. 67 | 68 | Now let's also say you also want to send a response back. That also means a delay in processing and lots of complications with making sure all the data is there before picking up the response. Most likely, this sort of thing is done using a cron job running every minute. Not only is this wasteful of network resources, it creates large, error-prone hurdles in communicating data between the two hosts. 69 | 70 | The Remoted API Server allows a server to run behind a firewall but lets public resources connect to it via another server that is accessible without having to open ports on the firewall. As long as the server/client security tokens stay secure, then it is extremely unlikely that an attacker can get in to do bad things. The only requirement here is that the host behind the firewall is able connect to a standard web server operating outside of the firewall. 71 | 72 | This approach also allows API servers like [Cloud Storage Server](https://github.com/cubiclesoft/cloud-storage-server) to become roaming API servers. The server itself can relocate to another host at a moment's notice with only a minimal interruption in service. If clients have built-in retry with random exponential fallback, then the disruption will barely be noticed. In addition, very complex scenarios such as whitelisted outbound ports, blocked incoming ports, and changing IP addresses can be overcome with relative ease. 73 | 74 | Protocol Schemes 75 | ---------------- 76 | 77 | The 'rws://' and 'rwss://' (SSL) protocol schemes are recommended for servers connecting to the Remoted API Server over WebSocket to be able to clearly identify the protocol. The recommended URL format is: `rwss://serverapikey@host/webroutepath`. Where 'webroutepath' is the path that clients will use to contact the server. If a server is already connected to the incoming path, Remoted API Server will reject the connection. 78 | 79 | The 'rwr://' and 'rwrs://' (SSL) protocol schemes are recommended for servers connecting to the Remoted API Server over WebRoute to be able to clearly identify the protocol. The recommended URL format is: `rwrs://clientapikey@host/webroutepath`. Where 'webroutepath' is the same path as the 'webroutepath' used by the server - that is, so the Remoted API Server knows which server to connect the client to. 80 | 81 | Security Notes 82 | -------------- 83 | 84 | This server is designed to completely circumvent firewall infrastructure. There's no finer way of putting that. Like most of the sage advice regarding network-enabled software that can be used on the Internet: Caution is advised when deploying Remoted API Server to production systems. Know what you are doing when it comes to using software like Remoted API Server and its SDK or it'll come back to haunt you. 85 | 86 | Two separate installations of Remoted API Server on a single host allows for "private/internal-only" and "public-ish" API key pairs. This allows for isolation where some servers are hosted externally and some are hosted internally but both are handled through the same intermediate web host on which the Remoted API Servers run. 87 | -------------------------------------------------------------------------------- /sdks/php/deflate_stream.php: -------------------------------------------------------------------------------- 1 | open = false; 13 | } 14 | 15 | public function __destruct() 16 | { 17 | $this->Finalize(); 18 | } 19 | 20 | public static function IsSupported() 21 | { 22 | if (!is_bool(self::$supported)) 23 | { 24 | self::$supported = function_exists("stream_filter_append") && function_exists("stream_filter_remove") && function_exists("gzcompress"); 25 | if (self::$supported) 26 | { 27 | $data = self::Compress("test"); 28 | if ($data === false || $data === "") self::$supported = false; 29 | else 30 | { 31 | $data = self::Uncompress($data); 32 | if ($data === false || $data !== "test") self::$supported = false; 33 | } 34 | } 35 | } 36 | 37 | return self::$supported; 38 | } 39 | 40 | public static function Compress($data, $compresslevel = -1, $options = array()) 41 | { 42 | $ds = new DeflateStream; 43 | if (!$ds->Init("wb", $compresslevel, $options)) return false; 44 | if (!$ds->Write($data)) return false; 45 | if (!$ds->Finalize()) return false; 46 | $data = $ds->Read(); 47 | 48 | return $data; 49 | } 50 | 51 | public static function Uncompress($data, $options = array("type" => "auto")) 52 | { 53 | $ds = new DeflateStream; 54 | if (!$ds->Init("rb", -1, $options)) return false; 55 | if (!$ds->Write($data)) return false; 56 | if (!$ds->Finalize()) return false; 57 | $data = $ds->Read(); 58 | 59 | return $data; 60 | } 61 | 62 | public function Init($mode, $compresslevel = -1, $options = array()) 63 | { 64 | if ($mode !== "rb" && $mode !== "wb") return false; 65 | if ($this->open) $this->Finalize(); 66 | 67 | $this->fp = fopen("php://memory", "w+b"); 68 | if ($this->fp === false) return false; 69 | $this->compress = ($mode == "wb"); 70 | if (!isset($options["type"])) $options["type"] = "rfc1951"; 71 | 72 | if ($options["type"] == "rfc1950") $options["type"] = "zlib"; 73 | else if ($options["type"] == "rfc1952") $options["type"] = "gzip"; 74 | 75 | if ($options["type"] != "zlib" && $options["type"] != "gzip" && ($this->compress || $options["type"] != "auto")) $options["type"] = "raw"; 76 | $this->options = $options; 77 | 78 | // Add the deflate filter. 79 | if ($this->compress) $this->filter = stream_filter_append($this->fp, "zlib.deflate", STREAM_FILTER_WRITE, $compresslevel); 80 | else $this->filter = stream_filter_append($this->fp, "zlib.inflate", STREAM_FILTER_READ); 81 | 82 | $this->open = true; 83 | $this->indata = ""; 84 | $this->outdata = ""; 85 | 86 | if ($this->compress) 87 | { 88 | if ($this->options["type"] == "zlib") 89 | { 90 | $this->outdata .= "\x78\x9C"; 91 | $this->options["a"] = 1; 92 | $this->options["b"] = 0; 93 | } 94 | else if ($this->options["type"] == "gzip") 95 | { 96 | if (!class_exists("CRC32Stream", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/crc32_stream.php"; 97 | 98 | $this->options["crc32"] = new CRC32Stream(); 99 | $this->options["crc32"]->Init(); 100 | $this->options["bytes"] = 0; 101 | 102 | $this->outdata .= "\x1F\x8B\x08"; 103 | $flags = 0; 104 | if (isset($this->options["filename"])) $flags |= 0x08; 105 | if (isset($this->options["comment"])) $flags |= 0x10; 106 | $this->outdata .= chr($flags); 107 | $this->outdata .= "\x00\x00\x00\x00"; 108 | $this->outdata .= "\x00"; 109 | $this->outdata .= "\x03"; 110 | 111 | if (isset($this->options["filename"])) $this->outdata .= str_replace("\x00", " ", $this->options["filename"]) . "\x00"; 112 | if (isset($this->options["comment"])) $this->outdata .= str_replace("\x00", " ", $this->options["comment"]) . "\x00"; 113 | } 114 | } 115 | else 116 | { 117 | $this->options["header"] = false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public function Read() 124 | { 125 | $result = $this->outdata; 126 | $this->outdata = ""; 127 | 128 | return $result; 129 | } 130 | 131 | public function Write($data) 132 | { 133 | if (!$this->open) return false; 134 | 135 | if ($this->compress) 136 | { 137 | if ($this->options["type"] == "zlib") 138 | { 139 | // Adler-32. 140 | $y = strlen($data); 141 | for ($x = 0; $x < $y; $x++) 142 | { 143 | $this->options["a"] = ($this->options["a"] + ord($data[$x])) % 65521; 144 | $this->options["b"] = ($this->options["b"] + $this->options["a"]) % 65521; 145 | } 146 | } 147 | else if ($this->options["type"] == "gzip") 148 | { 149 | $this->options["crc32"]->AddData($data); 150 | $this->options["bytes"] = $this->ADD32($this->options["bytes"], strlen($data)); 151 | } 152 | 153 | $this->indata .= $data; 154 | while (strlen($this->indata) >= 65536) 155 | { 156 | fwrite($this->fp, substr($this->indata, 0, 65536)); 157 | $this->indata = substr($this->indata, 65536); 158 | 159 | $this->ProcessOutput(); 160 | } 161 | } 162 | else 163 | { 164 | $this->indata .= $data; 165 | $this->ProcessInput(); 166 | } 167 | 168 | return true; 169 | } 170 | 171 | // Finalizes the stream. 172 | public function Finalize() 173 | { 174 | if (!$this->open) return false; 175 | 176 | if (!$this->compress) $this->ProcessInput(true); 177 | 178 | if (strlen($this->indata) > 0) 179 | { 180 | fwrite($this->fp, $this->indata); 181 | $this->indata = ""; 182 | } 183 | 184 | // Removing the filter pushes the last buffer into the stream. 185 | stream_filter_remove($this->filter); 186 | $this->filter = false; 187 | 188 | $this->ProcessOutput(); 189 | 190 | fclose($this->fp); 191 | 192 | if ($this->compress) 193 | { 194 | if ($this->options["type"] == "zlib") $this->outdata .= pack("N", $this->SHL32($this->options["b"], 16) | $this->options["a"]); 195 | else if ($this->options["type"] == "gzip") $this->outdata .= pack("V", $this->options["crc32"]->Finalize()) . pack("V", $this->options["bytes"]); 196 | } 197 | 198 | $this->open = false; 199 | 200 | return true; 201 | } 202 | 203 | private function ProcessOutput() 204 | { 205 | rewind($this->fp); 206 | 207 | // Hack! Because ftell() on a stream with a filter is still broken even under the latest PHP a mere 11 years later. 208 | // See: https://bugs.php.net/bug.php?id=49874 209 | ob_start(); 210 | fpassthru($this->fp); 211 | $this->outdata .= ob_get_contents(); 212 | ob_end_clean(); 213 | 214 | rewind($this->fp); 215 | ftruncate($this->fp, 0); 216 | } 217 | 218 | private function ProcessInput($final = false) 219 | { 220 | // Automatically determine the type of data based on the header signature. 221 | if ($this->options["type"] == "auto") 222 | { 223 | if (strlen($this->indata) >= 3) 224 | { 225 | $zlibtest = unpack("n", substr($this->indata, 0, 2)); 226 | 227 | if (substr($this->indata, 0, 3) === "\x1F\x8B\x08") $this->options["type"] = "gzip"; 228 | else if ((ord($this->indata[0]) & 0x0F) == 8 && ((ord($this->indata[0]) & 0xF0) >> 4) < 8 && $zlibtest[1] % 31 == 0) $this->options["type"] = "zlib"; 229 | else $this->options["type"] = "raw"; 230 | } 231 | else if ($final) $this->options["type"] = "raw"; 232 | } 233 | 234 | if ($this->options["type"] == "gzip") 235 | { 236 | if (!$this->options["header"]) 237 | { 238 | if (strlen($this->indata) >= 10) 239 | { 240 | $idcm = substr($this->indata, 0, 3); 241 | $flg = ord($this->indata[3]); 242 | 243 | if ($idcm !== "\x1F\x8B\x08") $this->options["type"] = "ignore"; 244 | else 245 | { 246 | // Calculate the number of bytes to skip. If flags are set, the size can be dynamic. 247 | $size = 10; 248 | $y = strlen($this->indata); 249 | 250 | // FLG.FEXTRA 251 | if ($size && ($flg & 0x04)) 252 | { 253 | if ($size + 2 >= $y) $size = 0; 254 | else 255 | { 256 | $xlen = unpack("v", substr($this->indata, $size, 2)); 257 | $size = ($size + 2 + $xlen <= $y ? $size + 2 + $xlen : 0); 258 | } 259 | } 260 | 261 | // FLG.FNAME 262 | if ($size && ($flg & 0x08)) 263 | { 264 | $pos = strpos($this->indata, "\x00", $size); 265 | $size = ($pos !== false ? $pos + 1 : 0); 266 | } 267 | 268 | // FLG.FCOMMENT 269 | if ($size && ($flg & 0x10)) 270 | { 271 | $pos = strpos($this->indata, "\x00", $size); 272 | $size = ($pos !== false ? $pos + 1 : 0); 273 | } 274 | 275 | // FLG.FHCRC 276 | if ($size && ($flg & 0x02)) $size = ($size + 2 <= $y ? $size + 2 : 0); 277 | 278 | if ($size) 279 | { 280 | $this->indata = substr($this->indata, $size); 281 | $this->options["header"] = true; 282 | } 283 | } 284 | } 285 | } 286 | 287 | if ($this->options["header"] && strlen($this->indata) > 8) 288 | { 289 | fwrite($this->fp, substr($this->indata, 0, -8)); 290 | $this->indata = substr($this->indata, -8); 291 | 292 | $this->ProcessOutput(); 293 | } 294 | 295 | if ($final) $this->indata = ""; 296 | } 297 | else if ($this->options["type"] == "zlib") 298 | { 299 | if (!$this->options["header"]) 300 | { 301 | if (strlen($this->indata) >= 2) 302 | { 303 | $cmf = ord($this->indata[0]); 304 | $flg = ord($this->indata[1]); 305 | $cm = $cmf & 0x0F; 306 | $cinfo = ($cmf & 0xF0) >> 4; 307 | 308 | // Compression method 'deflate' ($cm = 8), window size - 8 ($cinfo < 8), no preset dictionaries ($flg bit 5), checksum validates. 309 | if ($cm != 8 || $cinfo > 7 || ($flg & 0x20) || (($cmf << 8 | $flg) % 31) != 0) $this->options["type"] = "ignore"; 310 | else 311 | { 312 | $this->indata = substr($this->indata, 2); 313 | $this->options["header"] = true; 314 | } 315 | } 316 | } 317 | 318 | if ($this->options["header"] && strlen($this->indata) > 4) 319 | { 320 | fwrite($this->fp, substr($this->indata, 0, -4)); 321 | $this->indata = substr($this->indata, -4); 322 | 323 | $this->ProcessOutput(); 324 | } 325 | 326 | if ($final) $this->indata = ""; 327 | } 328 | 329 | if ($this->options["type"] == "raw") 330 | { 331 | fwrite($this->fp, $this->indata); 332 | $this->indata = ""; 333 | 334 | $this->ProcessOutput(); 335 | } 336 | 337 | // Only set when an unrecoverable header error has occurred for gzip or zlib. 338 | if ($this->options["type"] == "ignore") $this->indata = ""; 339 | } 340 | 341 | private function SHL32($num, $bits) 342 | { 343 | if ($bits < 0) $bits = 0; 344 | 345 | return $this->LIM32((int)$num << $bits); 346 | } 347 | 348 | private function LIM32($num) 349 | { 350 | return (int)((int)$num & 0xFFFFFFFF); 351 | } 352 | 353 | private function ADD32($num, $num2) 354 | { 355 | $num = (int)$num; 356 | $num2 = (int)$num2; 357 | $add = ((($num >> 30) & 0x03) + (($num2 >> 30) & 0x03)); 358 | $num = ((int)($num & 0x3FFFFFFF) + (int)($num2 & 0x3FFFFFFF)); 359 | if ($num & 0x40000000) $add++; 360 | $num = (int)(($num & 0x3FFFFFFF) | (($add & 0x03) << 30)); 361 | 362 | return $num; 363 | } 364 | } 365 | ?> -------------------------------------------------------------------------------- /support/deflate_stream.php: -------------------------------------------------------------------------------- 1 | open = false; 13 | } 14 | 15 | public function __destruct() 16 | { 17 | $this->Finalize(); 18 | } 19 | 20 | public static function IsSupported() 21 | { 22 | if (!is_bool(self::$supported)) 23 | { 24 | self::$supported = function_exists("stream_filter_append") && function_exists("stream_filter_remove") && function_exists("gzcompress"); 25 | if (self::$supported) 26 | { 27 | $data = self::Compress("test"); 28 | if ($data === false || $data === "") self::$supported = false; 29 | else 30 | { 31 | $data = self::Uncompress($data); 32 | if ($data === false || $data !== "test") self::$supported = false; 33 | } 34 | } 35 | } 36 | 37 | return self::$supported; 38 | } 39 | 40 | public static function Compress($data, $compresslevel = -1, $options = array()) 41 | { 42 | $ds = new DeflateStream; 43 | if (!$ds->Init("wb", $compresslevel, $options)) return false; 44 | if (!$ds->Write($data)) return false; 45 | if (!$ds->Finalize()) return false; 46 | $data = $ds->Read(); 47 | 48 | return $data; 49 | } 50 | 51 | public static function Uncompress($data, $options = array("type" => "auto")) 52 | { 53 | $ds = new DeflateStream; 54 | if (!$ds->Init("rb", -1, $options)) return false; 55 | if (!$ds->Write($data)) return false; 56 | if (!$ds->Finalize()) return false; 57 | $data = $ds->Read(); 58 | 59 | return $data; 60 | } 61 | 62 | public function Init($mode, $compresslevel = -1, $options = array()) 63 | { 64 | if ($mode !== "rb" && $mode !== "wb") return false; 65 | if ($this->open) $this->Finalize(); 66 | 67 | $this->fp = fopen("php://memory", "w+b"); 68 | if ($this->fp === false) return false; 69 | $this->compress = ($mode == "wb"); 70 | if (!isset($options["type"])) $options["type"] = "rfc1951"; 71 | 72 | if ($options["type"] == "rfc1950") $options["type"] = "zlib"; 73 | else if ($options["type"] == "rfc1952") $options["type"] = "gzip"; 74 | 75 | if ($options["type"] != "zlib" && $options["type"] != "gzip" && ($this->compress || $options["type"] != "auto")) $options["type"] = "raw"; 76 | $this->options = $options; 77 | 78 | // Add the deflate filter. 79 | if ($this->compress) $this->filter = stream_filter_append($this->fp, "zlib.deflate", STREAM_FILTER_WRITE, $compresslevel); 80 | else $this->filter = stream_filter_append($this->fp, "zlib.inflate", STREAM_FILTER_READ); 81 | 82 | $this->open = true; 83 | $this->indata = ""; 84 | $this->outdata = ""; 85 | 86 | if ($this->compress) 87 | { 88 | if ($this->options["type"] == "zlib") 89 | { 90 | $this->outdata .= "\x78\x9C"; 91 | $this->options["a"] = 1; 92 | $this->options["b"] = 0; 93 | } 94 | else if ($this->options["type"] == "gzip") 95 | { 96 | if (!class_exists("CRC32Stream", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/crc32_stream.php"; 97 | 98 | $this->options["crc32"] = new CRC32Stream(); 99 | $this->options["crc32"]->Init(); 100 | $this->options["bytes"] = 0; 101 | 102 | $this->outdata .= "\x1F\x8B\x08"; 103 | $flags = 0; 104 | if (isset($this->options["filename"])) $flags |= 0x08; 105 | if (isset($this->options["comment"])) $flags |= 0x10; 106 | $this->outdata .= chr($flags); 107 | $this->outdata .= "\x00\x00\x00\x00"; 108 | $this->outdata .= "\x00"; 109 | $this->outdata .= "\x03"; 110 | 111 | if (isset($this->options["filename"])) $this->outdata .= str_replace("\x00", " ", $this->options["filename"]) . "\x00"; 112 | if (isset($this->options["comment"])) $this->outdata .= str_replace("\x00", " ", $this->options["comment"]) . "\x00"; 113 | } 114 | } 115 | else 116 | { 117 | $this->options["header"] = false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public function Read() 124 | { 125 | $result = $this->outdata; 126 | $this->outdata = ""; 127 | 128 | return $result; 129 | } 130 | 131 | public function Write($data) 132 | { 133 | if (!$this->open) return false; 134 | 135 | if ($this->compress) 136 | { 137 | if ($this->options["type"] == "zlib") 138 | { 139 | // Adler-32. 140 | $y = strlen($data); 141 | for ($x = 0; $x < $y; $x++) 142 | { 143 | $this->options["a"] = ($this->options["a"] + ord($data[$x])) % 65521; 144 | $this->options["b"] = ($this->options["b"] + $this->options["a"]) % 65521; 145 | } 146 | } 147 | else if ($this->options["type"] == "gzip") 148 | { 149 | $this->options["crc32"]->AddData($data); 150 | $this->options["bytes"] = $this->ADD32($this->options["bytes"], strlen($data)); 151 | } 152 | 153 | $this->indata .= $data; 154 | while (strlen($this->indata) >= 65536) 155 | { 156 | fwrite($this->fp, substr($this->indata, 0, 65536)); 157 | $this->indata = substr($this->indata, 65536); 158 | 159 | $this->ProcessOutput(); 160 | } 161 | } 162 | else 163 | { 164 | $this->indata .= $data; 165 | $this->ProcessInput(); 166 | } 167 | 168 | return true; 169 | } 170 | 171 | // Finalizes the stream. 172 | public function Finalize() 173 | { 174 | if (!$this->open) return false; 175 | 176 | if (!$this->compress) $this->ProcessInput(true); 177 | 178 | if (strlen($this->indata) > 0) 179 | { 180 | fwrite($this->fp, $this->indata); 181 | $this->indata = ""; 182 | } 183 | 184 | // Removing the filter pushes the last buffer into the stream. 185 | stream_filter_remove($this->filter); 186 | $this->filter = false; 187 | 188 | $this->ProcessOutput(); 189 | 190 | fclose($this->fp); 191 | 192 | if ($this->compress) 193 | { 194 | if ($this->options["type"] == "zlib") $this->outdata .= pack("N", $this->SHL32($this->options["b"], 16) | $this->options["a"]); 195 | else if ($this->options["type"] == "gzip") $this->outdata .= pack("V", $this->options["crc32"]->Finalize()) . pack("V", $this->options["bytes"]); 196 | } 197 | 198 | $this->open = false; 199 | 200 | return true; 201 | } 202 | 203 | private function ProcessOutput() 204 | { 205 | rewind($this->fp); 206 | 207 | // Hack! Because ftell() on a stream with a filter is still broken even under the latest PHP a mere 11 years later. 208 | // See: https://bugs.php.net/bug.php?id=49874 209 | ob_start(); 210 | fpassthru($this->fp); 211 | $this->outdata .= ob_get_contents(); 212 | ob_end_clean(); 213 | 214 | rewind($this->fp); 215 | ftruncate($this->fp, 0); 216 | } 217 | 218 | private function ProcessInput($final = false) 219 | { 220 | // Automatically determine the type of data based on the header signature. 221 | if ($this->options["type"] == "auto") 222 | { 223 | if (strlen($this->indata) >= 3) 224 | { 225 | $zlibtest = unpack("n", substr($this->indata, 0, 2)); 226 | 227 | if (substr($this->indata, 0, 3) === "\x1F\x8B\x08") $this->options["type"] = "gzip"; 228 | else if ((ord($this->indata[0]) & 0x0F) == 8 && ((ord($this->indata[0]) & 0xF0) >> 4) < 8 && $zlibtest[1] % 31 == 0) $this->options["type"] = "zlib"; 229 | else $this->options["type"] = "raw"; 230 | } 231 | else if ($final) $this->options["type"] = "raw"; 232 | } 233 | 234 | if ($this->options["type"] == "gzip") 235 | { 236 | if (!$this->options["header"]) 237 | { 238 | if (strlen($this->indata) >= 10) 239 | { 240 | $idcm = substr($this->indata, 0, 3); 241 | $flg = ord($this->indata[3]); 242 | 243 | if ($idcm !== "\x1F\x8B\x08") $this->options["type"] = "ignore"; 244 | else 245 | { 246 | // Calculate the number of bytes to skip. If flags are set, the size can be dynamic. 247 | $size = 10; 248 | $y = strlen($this->indata); 249 | 250 | // FLG.FEXTRA 251 | if ($size && ($flg & 0x04)) 252 | { 253 | if ($size + 2 >= $y) $size = 0; 254 | else 255 | { 256 | $xlen = unpack("v", substr($this->indata, $size, 2)); 257 | $size = ($size + 2 + $xlen <= $y ? $size + 2 + $xlen : 0); 258 | } 259 | } 260 | 261 | // FLG.FNAME 262 | if ($size && ($flg & 0x08)) 263 | { 264 | $pos = strpos($this->indata, "\x00", $size); 265 | $size = ($pos !== false ? $pos + 1 : 0); 266 | } 267 | 268 | // FLG.FCOMMENT 269 | if ($size && ($flg & 0x10)) 270 | { 271 | $pos = strpos($this->indata, "\x00", $size); 272 | $size = ($pos !== false ? $pos + 1 : 0); 273 | } 274 | 275 | // FLG.FHCRC 276 | if ($size && ($flg & 0x02)) $size = ($size + 2 <= $y ? $size + 2 : 0); 277 | 278 | if ($size) 279 | { 280 | $this->indata = substr($this->indata, $size); 281 | $this->options["header"] = true; 282 | } 283 | } 284 | } 285 | } 286 | 287 | if ($this->options["header"] && strlen($this->indata) > 8) 288 | { 289 | fwrite($this->fp, substr($this->indata, 0, -8)); 290 | $this->indata = substr($this->indata, -8); 291 | 292 | $this->ProcessOutput(); 293 | } 294 | 295 | if ($final) $this->indata = ""; 296 | } 297 | else if ($this->options["type"] == "zlib") 298 | { 299 | if (!$this->options["header"]) 300 | { 301 | if (strlen($this->indata) >= 2) 302 | { 303 | $cmf = ord($this->indata[0]); 304 | $flg = ord($this->indata[1]); 305 | $cm = $cmf & 0x0F; 306 | $cinfo = ($cmf & 0xF0) >> 4; 307 | 308 | // Compression method 'deflate' ($cm = 8), window size - 8 ($cinfo < 8), no preset dictionaries ($flg bit 5), checksum validates. 309 | if ($cm != 8 || $cinfo > 7 || ($flg & 0x20) || (($cmf << 8 | $flg) % 31) != 0) $this->options["type"] = "ignore"; 310 | else 311 | { 312 | $this->indata = substr($this->indata, 2); 313 | $this->options["header"] = true; 314 | } 315 | } 316 | } 317 | 318 | if ($this->options["header"] && strlen($this->indata) > 4) 319 | { 320 | fwrite($this->fp, substr($this->indata, 0, -4)); 321 | $this->indata = substr($this->indata, -4); 322 | 323 | $this->ProcessOutput(); 324 | } 325 | 326 | if ($final) $this->indata = ""; 327 | } 328 | 329 | if ($this->options["type"] == "raw") 330 | { 331 | fwrite($this->fp, $this->indata); 332 | $this->indata = ""; 333 | 334 | $this->ProcessOutput(); 335 | } 336 | 337 | // Only set when an unrecoverable header error has occurred for gzip or zlib. 338 | if ($this->options["type"] == "ignore") $this->indata = ""; 339 | } 340 | 341 | private function SHL32($num, $bits) 342 | { 343 | if ($bits < 0) $bits = 0; 344 | 345 | return $this->LIM32((int)$num << $bits); 346 | } 347 | 348 | private function LIM32($num) 349 | { 350 | return (int)((int)$num & 0xFFFFFFFF); 351 | } 352 | 353 | private function ADD32($num, $num2) 354 | { 355 | $num = (int)$num; 356 | $num2 = (int)$num2; 357 | $add = ((($num >> 30) & 0x03) + (($num2 >> 30) & 0x03)); 358 | $num = ((int)($num & 0x3FFFFFFF) + (int)($num2 & 0x3FFFFFFF)); 359 | if ($num & 0x40000000) $add++; 360 | $num = (int)(($num & 0x3FFFFFFF) | (($add & 0x03) << 30)); 361 | 362 | return $num; 363 | } 364 | } 365 | ?> -------------------------------------------------------------------------------- /sdks/php/random.php: -------------------------------------------------------------------------------- 1 | mode = false; 15 | $this->fp = false; 16 | $this->cryptosafe = $cryptosafe; 17 | 18 | // Native first (PHP 7 and later). 19 | if (function_exists("random_bytes")) $this->mode = "native"; 20 | 21 | // OpenSSL fallback. 22 | if ($this->mode === false && function_exists("openssl_random_pseudo_bytes")) 23 | { 24 | // PHP 5.4.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for performance. 25 | @openssl_random_pseudo_bytes(4, $strong); 26 | if ($strong) $this->mode = "openssl"; 27 | } 28 | 29 | // Locate a (relatively) suitable source of entropy or raise an exception. 30 | if (strtoupper(substr(PHP_OS, 0, 3)) === "WIN") 31 | { 32 | // PHP 5.3.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for functionality. 33 | if ($this->mode === false && PHP_VERSION_ID > 50300 && function_exists("mcrypt_create_iv")) $this->mode = "mcrypt"; 34 | } 35 | else 36 | { 37 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/arandom")) 38 | { 39 | // OpenBSD. mcrypt doesn't attempt to use this despite claims of higher quality entropy with performance. 40 | $this->fp = @fopen("/dev/arandom", "rb"); 41 | if ($this->fp !== false) $this->mode = "file"; 42 | } 43 | 44 | if ($cryptosafe && $this->mode === false && file_exists("/dev/random")) 45 | { 46 | // Everything else. 47 | $this->fp = @fopen("/dev/random", "rb"); 48 | if ($this->fp !== false) $this->mode = "file"; 49 | } 50 | 51 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/urandom")) 52 | { 53 | // Everything else. 54 | $this->fp = @fopen("/dev/urandom", "rb"); 55 | if ($this->fp !== false) $this->mode = "file"; 56 | } 57 | 58 | if ($this->mode === false && function_exists("mcrypt_create_iv")) 59 | { 60 | // mcrypt_create_iv() is last because it opens and closes a file handle every single call. 61 | $this->mode = "mcrypt"; 62 | } 63 | } 64 | 65 | // Throw an exception if unable to find a suitable entropy source. 66 | if ($this->mode === false) 67 | { 68 | throw new Exception(self::RNG_Translate("Unable to locate a suitable entropy source.")); 69 | exit(); 70 | } 71 | } 72 | 73 | public function __destruct() 74 | { 75 | if ($this->mode === "file") fclose($this->fp); 76 | } 77 | 78 | public function GetBytes($length) 79 | { 80 | if ($this->mode === false) return false; 81 | 82 | $length = (int)$length; 83 | if ($length < 1) return false; 84 | 85 | $result = ""; 86 | do 87 | { 88 | switch ($this->mode) 89 | { 90 | case "native": $data = @random_bytes($length); break; 91 | case "openssl": $data = @openssl_random_pseudo_bytes($length, $strong); if (!$strong) $data = false; break; 92 | case "mcrypt": $data = @mcrypt_create_iv($length, ($this->cryptosafe ? MCRYPT_DEV_RANDOM : MCRYPT_DEV_URANDOM)); break; 93 | case "file": $data = @fread($this->fp, $length); break; 94 | default: $data = false; 95 | } 96 | if ($data === false) return false; 97 | 98 | $result .= $data; 99 | } while (strlen($result) < $length); 100 | 101 | return substr($result, 0, $length); 102 | } 103 | 104 | public function GenerateToken($length = 64) 105 | { 106 | $data = $this->GetBytes($length); 107 | if ($data === false) return false; 108 | 109 | return bin2hex($data); 110 | } 111 | 112 | // Get a random number between $min and $max (inclusive). 113 | public function GetInt($min, $max) 114 | { 115 | $min = (int)$min; 116 | $max = (int)$max; 117 | if ($max < $min) return false; 118 | if ($min == $max) return $min; 119 | 120 | $range = $max - $min + 1; 121 | 122 | $bits = 1; 123 | while ((1 << $bits) <= $range) $bits++; 124 | 125 | $numbytes = (int)(($bits + 7) / 8); 126 | $mask = (1 << $bits) - 1; 127 | 128 | do 129 | { 130 | $data = $this->GetBytes($numbytes); 131 | if ($data === false) return false; 132 | 133 | $result = 0; 134 | for ($x = 0; $x < $numbytes; $x++) 135 | { 136 | $result = ($result * 256) + ord($data[$x]); 137 | } 138 | 139 | $result = $result & $mask; 140 | } while ($result >= $range); 141 | 142 | return $result + $min; 143 | } 144 | 145 | // Convenience method to generate a random alphanumeric string. 146 | public function GenerateString($size = 32) 147 | { 148 | $result = ""; 149 | for ($x = 0; $x < $size; $x++) 150 | { 151 | $data = $this->GetInt(0, 61); 152 | if ($data === false) return false; 153 | 154 | $result .= self::$alphanum[$data]; 155 | } 156 | 157 | return $result; 158 | } 159 | 160 | public function GenerateWordLite(&$freqmap, $len) 161 | { 162 | $totalc = 0; 163 | $totalv = 0; 164 | foreach ($freqmap["consonants"] as $chr => $num) $totalc += $num; 165 | foreach ($freqmap["vowels"] as $chr => $num) $totalv += $num; 166 | 167 | if ($totalc <= 0 || $totalv <= 0) return false; 168 | 169 | $result = ""; 170 | for ($x = 0; $x < $len; $x++) 171 | { 172 | if ($x % 2) 173 | { 174 | $data = $this->GetInt(0, $totalv - 1); 175 | if ($data === false) return false; 176 | 177 | foreach ($freqmap["vowels"] as $chr => $num) 178 | { 179 | if ($num > $data) 180 | { 181 | $result .= $chr; 182 | 183 | break; 184 | } 185 | 186 | $data -= $num; 187 | } 188 | } 189 | else 190 | { 191 | $data = $this->GetInt(0, $totalc - 1); 192 | if ($data === false) return false; 193 | 194 | foreach ($freqmap["consonants"] as $chr => $num) 195 | { 196 | if ($num > $data) 197 | { 198 | $result .= $chr; 199 | 200 | break; 201 | } 202 | 203 | $data -= $num; 204 | } 205 | } 206 | } 207 | 208 | return $result; 209 | } 210 | 211 | public function GenerateWord(&$freqmap, $len, $separator = "-") 212 | { 213 | $result = ""; 214 | $queue = array(); 215 | $threshold = $freqmap["threshold"]; 216 | $state = "start"; 217 | while ($len) 218 | { 219 | //echo $state . " - " . $len . ": " . $result . "\n"; 220 | switch ($state) 221 | { 222 | case "start": 223 | { 224 | // The start of the word (or restart). 225 | $path = &$freqmap["start"]; 226 | while (count($queue) < $threshold && $len) 227 | { 228 | if ($len > 1 || !$path["*"]) 229 | { 230 | // Some part of the word. 231 | $found = false; 232 | if ($path[""]) 233 | { 234 | $pos = $this->GetInt(0, $path[""] - 1); 235 | 236 | foreach ($path as $chr => &$info) 237 | { 238 | if (!is_array($info)) continue; 239 | 240 | if ($info["+"] > $pos) 241 | { 242 | $result .= $chr; 243 | $queue[] = $chr; 244 | $path = &$path[$chr]; 245 | $len--; 246 | 247 | $found = true; 248 | 249 | break; 250 | } 251 | 252 | $pos -= $info["+"]; 253 | } 254 | } 255 | 256 | if (!$found) 257 | { 258 | $state = (count($queue) ? "recovery" : "restart"); 259 | 260 | break; 261 | } 262 | } 263 | else 264 | { 265 | // Last letter of the word. 266 | $found = false; 267 | if ($path["*"]) 268 | { 269 | $pos = $this->GetInt(0, $path["*"] - 1); 270 | 271 | foreach ($path as $chr => &$info) 272 | { 273 | if (!is_array($info)) continue; 274 | 275 | if ($info["-"] > $pos) 276 | { 277 | $result .= $chr; 278 | $queue[] = $chr; 279 | $path = &$path[$chr]; 280 | $len--; 281 | 282 | $found = true; 283 | 284 | break; 285 | } 286 | 287 | $pos -= $info["-"]; 288 | } 289 | } 290 | 291 | if (!$found) 292 | { 293 | $state = (count($queue) ? "end" : "restart"); 294 | 295 | break; 296 | } 297 | } 298 | } 299 | 300 | if (count($queue) >= $threshold) $state = ($len >= $threshold ? "middle" : "end"); 301 | 302 | break; 303 | } 304 | case "middle": 305 | { 306 | // The middle of the word. 307 | $str = implode("", $queue); 308 | 309 | if (!isset($freqmap["middle"][$str])) $state = "recovery"; 310 | else 311 | { 312 | $found = false; 313 | 314 | if ($freqmap["middle"][$str][""]) 315 | { 316 | $pos = $this->GetInt(0, $freqmap["middle"][$str][""] - 1); 317 | 318 | foreach ($freqmap["middle"][$str] as $chr => $num) 319 | { 320 | if ($chr === "") continue; 321 | 322 | if ($num > $pos) 323 | { 324 | $result .= $chr; 325 | $queue[] = $chr; 326 | array_shift($queue); 327 | $len--; 328 | 329 | if ($len < $threshold) $state = "end"; 330 | 331 | $found = true; 332 | 333 | break; 334 | } 335 | 336 | $pos -= $num; 337 | } 338 | } 339 | 340 | if (!$found) $state = "recovery"; 341 | } 342 | 343 | break; 344 | } 345 | case "end": 346 | { 347 | if (!isset($freqmap["end"][$len]) || !count($queue) || !isset($freqmap["end"][$len][$queue[count($queue) - 1]])) $state = "restart"; 348 | else 349 | { 350 | $path = &$freqmap["end"][$len][$queue[count($queue) - 1]]; 351 | 352 | $found = false; 353 | 354 | if ($path[""]) 355 | { 356 | $pos = $this->GetInt(0, $path[""] - 1); 357 | 358 | foreach ($path as $str => $num) 359 | { 360 | if ($str === "") continue; 361 | 362 | if ($num > $pos) 363 | { 364 | $result .= $str; 365 | $len = 0; 366 | 367 | $found = true; 368 | 369 | break; 370 | } 371 | 372 | $pos -= $num; 373 | } 374 | } 375 | 376 | if (!$found) $state = "restart"; 377 | } 378 | 379 | break; 380 | } 381 | case "recovery": 382 | { 383 | if (!count($queue) || !isset($freqmap["recovery"][$queue[count($queue) - 1]])) $state = "restart"; 384 | else 385 | { 386 | $path = &$freqmap["recovery"][$queue[count($queue) - 1]]; 387 | 388 | $found = false; 389 | 390 | if ($path[""]) 391 | { 392 | $pos = $this->GetInt(0, $path[""] - 1); 393 | 394 | foreach ($path as $chr => $num) 395 | { 396 | if ($chr === "") continue; 397 | 398 | if ($num > $pos) 399 | { 400 | $result .= $chr; 401 | $queue[] = $chr; 402 | array_shift($queue); 403 | $len--; 404 | 405 | $state = ($len >= $threshold ? "middle" : "end"); 406 | 407 | $found = true; 408 | 409 | break; 410 | } 411 | 412 | $pos -= $num; 413 | } 414 | } 415 | 416 | if (!$found) $state = "restart"; 417 | } 418 | 419 | break; 420 | } 421 | case "restart": 422 | { 423 | $result .= $separator; 424 | $queue = array(); 425 | $len -= strlen($separator); 426 | 427 | $state = "start"; 428 | 429 | break; 430 | } 431 | } 432 | } 433 | 434 | return $result; 435 | } 436 | 437 | public function GetMode() 438 | { 439 | return $this->mode; 440 | } 441 | 442 | protected static function RNG_Translate() 443 | { 444 | $args = func_get_args(); 445 | if (!count($args)) return ""; 446 | 447 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 448 | } 449 | } 450 | ?> -------------------------------------------------------------------------------- /support/random.php: -------------------------------------------------------------------------------- 1 | mode = false; 15 | $this->fp = false; 16 | $this->cryptosafe = $cryptosafe; 17 | 18 | // Native first (PHP 7 and later). 19 | if (function_exists("random_bytes")) $this->mode = "native"; 20 | 21 | // OpenSSL fallback. 22 | if ($this->mode === false && function_exists("openssl_random_pseudo_bytes")) 23 | { 24 | // PHP 5.4.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for performance. 25 | @openssl_random_pseudo_bytes(4, $strong); 26 | if ($strong) $this->mode = "openssl"; 27 | } 28 | 29 | // Locate a (relatively) suitable source of entropy or raise an exception. 30 | if (strtoupper(substr(PHP_OS, 0, 3)) === "WIN") 31 | { 32 | // PHP 5.3.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for functionality. 33 | if ($this->mode === false && PHP_VERSION_ID > 50300 && function_exists("mcrypt_create_iv")) $this->mode = "mcrypt"; 34 | } 35 | else 36 | { 37 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/arandom")) 38 | { 39 | // OpenBSD. mcrypt doesn't attempt to use this despite claims of higher quality entropy with performance. 40 | $this->fp = @fopen("/dev/arandom", "rb"); 41 | if ($this->fp !== false) $this->mode = "file"; 42 | } 43 | 44 | if ($cryptosafe && $this->mode === false && file_exists("/dev/random")) 45 | { 46 | // Everything else. 47 | $this->fp = @fopen("/dev/random", "rb"); 48 | if ($this->fp !== false) $this->mode = "file"; 49 | } 50 | 51 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/urandom")) 52 | { 53 | // Everything else. 54 | $this->fp = @fopen("/dev/urandom", "rb"); 55 | if ($this->fp !== false) $this->mode = "file"; 56 | } 57 | 58 | if ($this->mode === false && function_exists("mcrypt_create_iv")) 59 | { 60 | // mcrypt_create_iv() is last because it opens and closes a file handle every single call. 61 | $this->mode = "mcrypt"; 62 | } 63 | } 64 | 65 | // Throw an exception if unable to find a suitable entropy source. 66 | if ($this->mode === false) 67 | { 68 | throw new Exception(self::RNG_Translate("Unable to locate a suitable entropy source.")); 69 | exit(); 70 | } 71 | } 72 | 73 | public function __destruct() 74 | { 75 | if ($this->mode === "file") fclose($this->fp); 76 | } 77 | 78 | public function GetBytes($length) 79 | { 80 | if ($this->mode === false) return false; 81 | 82 | $length = (int)$length; 83 | if ($length < 1) return false; 84 | 85 | $result = ""; 86 | do 87 | { 88 | switch ($this->mode) 89 | { 90 | case "native": $data = @random_bytes($length); break; 91 | case "openssl": $data = @openssl_random_pseudo_bytes($length, $strong); if (!$strong) $data = false; break; 92 | case "mcrypt": $data = @mcrypt_create_iv($length, ($this->cryptosafe ? MCRYPT_DEV_RANDOM : MCRYPT_DEV_URANDOM)); break; 93 | case "file": $data = @fread($this->fp, $length); break; 94 | default: $data = false; 95 | } 96 | if ($data === false) return false; 97 | 98 | $result .= $data; 99 | } while (strlen($result) < $length); 100 | 101 | return substr($result, 0, $length); 102 | } 103 | 104 | public function GenerateToken($length = 64) 105 | { 106 | $data = $this->GetBytes($length); 107 | if ($data === false) return false; 108 | 109 | return bin2hex($data); 110 | } 111 | 112 | // Get a random number between $min and $max (inclusive). 113 | public function GetInt($min, $max) 114 | { 115 | $min = (int)$min; 116 | $max = (int)$max; 117 | if ($max < $min) return false; 118 | if ($min == $max) return $min; 119 | 120 | $range = $max - $min + 1; 121 | 122 | $bits = 1; 123 | while ((1 << $bits) <= $range) $bits++; 124 | 125 | $numbytes = (int)(($bits + 7) / 8); 126 | $mask = (1 << $bits) - 1; 127 | 128 | do 129 | { 130 | $data = $this->GetBytes($numbytes); 131 | if ($data === false) return false; 132 | 133 | $result = 0; 134 | for ($x = 0; $x < $numbytes; $x++) 135 | { 136 | $result = ($result * 256) + ord($data[$x]); 137 | } 138 | 139 | $result = $result & $mask; 140 | } while ($result >= $range); 141 | 142 | return $result + $min; 143 | } 144 | 145 | // Convenience method to generate a random alphanumeric string. 146 | public function GenerateString($size = 32) 147 | { 148 | $result = ""; 149 | for ($x = 0; $x < $size; $x++) 150 | { 151 | $data = $this->GetInt(0, 61); 152 | if ($data === false) return false; 153 | 154 | $result .= self::$alphanum[$data]; 155 | } 156 | 157 | return $result; 158 | } 159 | 160 | public function GenerateWordLite(&$freqmap, $len) 161 | { 162 | $totalc = 0; 163 | $totalv = 0; 164 | foreach ($freqmap["consonants"] as $chr => $num) $totalc += $num; 165 | foreach ($freqmap["vowels"] as $chr => $num) $totalv += $num; 166 | 167 | if ($totalc <= 0 || $totalv <= 0) return false; 168 | 169 | $result = ""; 170 | for ($x = 0; $x < $len; $x++) 171 | { 172 | if ($x % 2) 173 | { 174 | $data = $this->GetInt(0, $totalv - 1); 175 | if ($data === false) return false; 176 | 177 | foreach ($freqmap["vowels"] as $chr => $num) 178 | { 179 | if ($num > $data) 180 | { 181 | $result .= $chr; 182 | 183 | break; 184 | } 185 | 186 | $data -= $num; 187 | } 188 | } 189 | else 190 | { 191 | $data = $this->GetInt(0, $totalc - 1); 192 | if ($data === false) return false; 193 | 194 | foreach ($freqmap["consonants"] as $chr => $num) 195 | { 196 | if ($num > $data) 197 | { 198 | $result .= $chr; 199 | 200 | break; 201 | } 202 | 203 | $data -= $num; 204 | } 205 | } 206 | } 207 | 208 | return $result; 209 | } 210 | 211 | public function GenerateWord(&$freqmap, $len, $separator = "-") 212 | { 213 | $result = ""; 214 | $queue = array(); 215 | $threshold = $freqmap["threshold"]; 216 | $state = "start"; 217 | while ($len) 218 | { 219 | //echo $state . " - " . $len . ": " . $result . "\n"; 220 | switch ($state) 221 | { 222 | case "start": 223 | { 224 | // The start of the word (or restart). 225 | $path = &$freqmap["start"]; 226 | while (count($queue) < $threshold && $len) 227 | { 228 | if ($len > 1 || !$path["*"]) 229 | { 230 | // Some part of the word. 231 | $found = false; 232 | if ($path[""]) 233 | { 234 | $pos = $this->GetInt(0, $path[""] - 1); 235 | 236 | foreach ($path as $chr => &$info) 237 | { 238 | if (!is_array($info)) continue; 239 | 240 | if ($info["+"] > $pos) 241 | { 242 | $result .= $chr; 243 | $queue[] = $chr; 244 | $path = &$path[$chr]; 245 | $len--; 246 | 247 | $found = true; 248 | 249 | break; 250 | } 251 | 252 | $pos -= $info["+"]; 253 | } 254 | } 255 | 256 | if (!$found) 257 | { 258 | $state = (count($queue) ? "recovery" : "restart"); 259 | 260 | break; 261 | } 262 | } 263 | else 264 | { 265 | // Last letter of the word. 266 | $found = false; 267 | if ($path["*"]) 268 | { 269 | $pos = $this->GetInt(0, $path["*"] - 1); 270 | 271 | foreach ($path as $chr => &$info) 272 | { 273 | if (!is_array($info)) continue; 274 | 275 | if ($info["-"] > $pos) 276 | { 277 | $result .= $chr; 278 | $queue[] = $chr; 279 | $path = &$path[$chr]; 280 | $len--; 281 | 282 | $found = true; 283 | 284 | break; 285 | } 286 | 287 | $pos -= $info["-"]; 288 | } 289 | } 290 | 291 | if (!$found) 292 | { 293 | $state = (count($queue) ? "end" : "restart"); 294 | 295 | break; 296 | } 297 | } 298 | } 299 | 300 | if (count($queue) >= $threshold) $state = ($len >= $threshold ? "middle" : "end"); 301 | 302 | break; 303 | } 304 | case "middle": 305 | { 306 | // The middle of the word. 307 | $str = implode("", $queue); 308 | 309 | if (!isset($freqmap["middle"][$str])) $state = "recovery"; 310 | else 311 | { 312 | $found = false; 313 | 314 | if ($freqmap["middle"][$str][""]) 315 | { 316 | $pos = $this->GetInt(0, $freqmap["middle"][$str][""] - 1); 317 | 318 | foreach ($freqmap["middle"][$str] as $chr => $num) 319 | { 320 | if ($chr === "") continue; 321 | 322 | if ($num > $pos) 323 | { 324 | $result .= $chr; 325 | $queue[] = $chr; 326 | array_shift($queue); 327 | $len--; 328 | 329 | if ($len < $threshold) $state = "end"; 330 | 331 | $found = true; 332 | 333 | break; 334 | } 335 | 336 | $pos -= $num; 337 | } 338 | } 339 | 340 | if (!$found) $state = "recovery"; 341 | } 342 | 343 | break; 344 | } 345 | case "end": 346 | { 347 | if (!isset($freqmap["end"][$len]) || !count($queue) || !isset($freqmap["end"][$len][$queue[count($queue) - 1]])) $state = "restart"; 348 | else 349 | { 350 | $path = &$freqmap["end"][$len][$queue[count($queue) - 1]]; 351 | 352 | $found = false; 353 | 354 | if ($path[""]) 355 | { 356 | $pos = $this->GetInt(0, $path[""] - 1); 357 | 358 | foreach ($path as $str => $num) 359 | { 360 | if ($str === "") continue; 361 | 362 | if ($num > $pos) 363 | { 364 | $result .= $str; 365 | $len = 0; 366 | 367 | $found = true; 368 | 369 | break; 370 | } 371 | 372 | $pos -= $num; 373 | } 374 | } 375 | 376 | if (!$found) $state = "restart"; 377 | } 378 | 379 | break; 380 | } 381 | case "recovery": 382 | { 383 | if (!count($queue) || !isset($freqmap["recovery"][$queue[count($queue) - 1]])) $state = "restart"; 384 | else 385 | { 386 | $path = &$freqmap["recovery"][$queue[count($queue) - 1]]; 387 | 388 | $found = false; 389 | 390 | if ($path[""]) 391 | { 392 | $pos = $this->GetInt(0, $path[""] - 1); 393 | 394 | foreach ($path as $chr => $num) 395 | { 396 | if ($chr === "") continue; 397 | 398 | if ($num > $pos) 399 | { 400 | $result .= $chr; 401 | $queue[] = $chr; 402 | array_shift($queue); 403 | $len--; 404 | 405 | $state = ($len >= $threshold ? "middle" : "end"); 406 | 407 | $found = true; 408 | 409 | break; 410 | } 411 | 412 | $pos -= $num; 413 | } 414 | } 415 | 416 | if (!$found) $state = "restart"; 417 | } 418 | 419 | break; 420 | } 421 | case "restart": 422 | { 423 | $result .= $separator; 424 | $queue = array(); 425 | $len -= strlen($separator); 426 | 427 | $state = "start"; 428 | 429 | break; 430 | } 431 | } 432 | } 433 | 434 | return $result; 435 | } 436 | 437 | public function GetMode() 438 | { 439 | return $this->mode; 440 | } 441 | 442 | protected static function RNG_Translate() 443 | { 444 | $args = func_get_args(); 445 | if (!count($args)) return ""; 446 | 447 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 448 | } 449 | } 450 | ?> -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 1) 22 | { 23 | // Service Manager PHP SDK. 24 | require_once $rootpath . "/support/servicemanager.php"; 25 | 26 | $sm = new ServiceManager($rootpath . "/servicemanager"); 27 | 28 | echo "Service manager: " . $sm->GetServiceManagerRealpath() . "\n\n"; 29 | 30 | $servicename = preg_replace('/[^a-z0-9]/', "-", $config["servicename"]); 31 | 32 | if ($argv[1] == "install") 33 | { 34 | // Install the service. 35 | $args = array(); 36 | $options = array( 37 | "nixuser" => $config["serviceuser"], 38 | "nixgroup" => $config["serviceuser"] 39 | ); 40 | 41 | $result = $sm->Install($servicename, __FILE__, $args, $options, true); 42 | if (!$result["success"]) CLI::DisplayError("Unable to install the '" . $servicename . "' service.", $result); 43 | } 44 | else if ($argv[1] == "start") 45 | { 46 | // Start the service. 47 | $result = $sm->Start($servicename, true); 48 | if (!$result["success"]) CLI::DisplayError("Unable to start the '" . $servicename . "' service.", $result); 49 | } 50 | else if ($argv[1] == "stop") 51 | { 52 | // Stop the service. 53 | $result = $sm->Stop($servicename, true); 54 | if (!$result["success"]) CLI::DisplayError("Unable to stop the '" . $servicename . "' service.", $result); 55 | } 56 | else if ($argv[1] == "uninstall") 57 | { 58 | // Uninstall the service. 59 | $result = $sm->Uninstall($servicename, true); 60 | if (!$result["success"]) CLI::DisplayError("Unable to uninstall the '" . $servicename . "' service.", $result); 61 | } 62 | else if ($argv[1] == "dumpconfig") 63 | { 64 | $result = $sm->GetConfig($servicename); 65 | if (!$result["success"]) CLI::DisplayError("Unable to retrieve the configuration for the '" . $servicename . "' service.", $result); 66 | 67 | echo "Service configuration: " . $result["filename"] . "\n\n"; 68 | 69 | echo "Current service configuration:\n\n"; 70 | foreach ($result["options"] as $key => $val) echo " " . $key . " = " . $val . "\n"; 71 | } 72 | else 73 | { 74 | echo "Command not recognized. Run the service manager directly for anything other than 'install', 'start', 'stop', 'uninstall', and 'dumpconfig'.\n"; 75 | } 76 | 77 | exit(); 78 | } 79 | 80 | // Start the main server. 81 | require_once $rootpath . "/support/web_server.php"; 82 | require_once $rootpath . "/support/websocket_server.php"; 83 | require_once $rootpath . "/support/webroute_server.php"; 84 | require_once $rootpath . "/support/random.php"; 85 | require_once $rootpath . "/support/str_basics.php"; 86 | 87 | $webserver = new WebServer(); 88 | $wsserver = new WebSocketServer(); 89 | $wrserver = new WebRouteServer(); 90 | 91 | $pathmap = array(); 92 | 93 | echo "Starting server...\n"; 94 | $result = $webserver->Start($config["host"], $config["port"], false); 95 | if (!$result["success"]) 96 | { 97 | var_dump($result); 98 | exit(); 99 | } 100 | 101 | echo "Ready.\n"; 102 | 103 | $stopfilename = __FILE__ . ".notify.stop"; 104 | $reloadfilename = __FILE__ . ".notify.reload"; 105 | $lastservicecheck = time(); 106 | $running = true; 107 | 108 | do 109 | { 110 | // Implement the stream_select() call directly since multiple server instances are involved. 111 | $timeout = 3; 112 | $readfps = array(); 113 | $writefps = array(); 114 | $exceptfps = NULL; 115 | 116 | $webserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 117 | $wsserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 118 | $wrserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 119 | 120 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 121 | if ($result === false) break; 122 | 123 | // Web server. 124 | $result = $webserver->Wait(0); 125 | 126 | // Handle active clients. 127 | foreach ($result["clients"] as $id => $client) 128 | { 129 | if ($client->appdata === false) 130 | { 131 | echo "Webserver client ID " . $id . " connected.\n"; 132 | 133 | $client->appdata = array("mode" => false, "path" => false); 134 | } 135 | 136 | // Check for a valid API key. 137 | if ($client->appdata["mode"] === false && isset($client->headers["X-Remoted-Apikey"])) 138 | { 139 | $apikey = $client->headers["X-Remoted-Apikey"]; 140 | 141 | if (Str::CTstrcmp($config["client_apikey"], $apikey) == 0) $client->appdata["mode"] = "client"; 142 | else if (Str::CTstrcmp($config["server_apikey"], $apikey) == 0) $client->appdata["mode"] = "server"; 143 | 144 | if ($client->appdata["mode"] !== false) echo "Valid " . $client->appdata["mode"] . " API key used.\n"; 145 | } 146 | 147 | if ($client->appdata["mode"] !== false && $client->appdata["path"] === false) 148 | { 149 | $url = HTTP::ExtractURL($client->url); 150 | $client->appdata["path"] = $url["path"]; 151 | } 152 | 153 | // Wait until the request is complete before fully processing inputs. 154 | if ($client->requestcomplete) 155 | { 156 | // Prevent proxies from doing bad things. 157 | $client->SetResponseNoCache(); 158 | 159 | if ($client->appdata["mode"] === false) 160 | { 161 | echo "Missing API key.\n"; 162 | 163 | $client->SetResponseCode(403); 164 | $client->SetResponseContentType("application/json"); 165 | $client->AddResponseContent(json_encode(array("success" => false, "error" => "Invalid or missing 'X-Remoted-APIKey' header.", "errorcode" => "invalid_missing_apikey"))); 166 | $client->FinalizeResponse(); 167 | } 168 | else if ($client->appdata["path"] === false) 169 | { 170 | echo "Unknown or invalid path.\n"; 171 | 172 | $client->SetResponseCode(403); 173 | $client->SetResponseContentType("application/json"); 174 | $client->AddResponseContent(json_encode(array("success" => false, "error" => "Unknown or invalid path. Bad request.", "errorcode" => "invalid_request_path"))); 175 | $client->FinalizeResponse(); 176 | } 177 | else if ($client->appdata["mode"] === "server") 178 | { 179 | // Handle WebRoute upgrade requests. 180 | $id2 = $wrserver->ProcessWebServerClientUpgrade($webserver, $client, true); 181 | if ($id2 !== false) 182 | { 183 | $client2 = $wrserver->GetClient($id2); 184 | 185 | // Clean up the waiting queue. 186 | if ($client2->linkid !== false) 187 | { 188 | $id3 = $pathmap[$client2->appdata["path"]]; 189 | $client3 = $wsserver->GetClient($id3); 190 | 191 | unset($client3->appdata["waiting"][$client2->linkid]); 192 | } 193 | 194 | echo "Webserver client ID " . $id . " upgraded to WebRoute. WebRoute client ID is " . $id2 . ".\n"; 195 | } 196 | else 197 | { 198 | // Handle WebSocket upgrade requests. 199 | if (!isset($pathmap[$client->appdata["path"]])) $id2 = $wsserver->ProcessWebServerClientUpgrade($webserver, $client); 200 | if ($id2 !== false) 201 | { 202 | $client2 = $wsserver->GetClient($id2); 203 | 204 | $client2->appdata["waiting"] = array(); 205 | 206 | $pathmap[$client2->appdata["path"]] = $id2; 207 | 208 | echo "Webserver client ID " . $id . " upgraded to WebSocket. WebSocket client ID is " . $id2 . ". Listening on '" . $client2->appdata["path"] . "'.\n"; 209 | } 210 | else 211 | { 212 | // Reject all other requests. 213 | $result2 = array("success" => false, "error" => "Invalid request. Expected WebSocket or WebRoute upgrade.", "errorcode" => "invalid_request"); 214 | 215 | $client->SetResponseCode(400); 216 | 217 | // Send the response. 218 | $client->SetResponseContentType("application/json"); 219 | $client->AddResponseContent(json_encode($result2)); 220 | $client->FinalizeResponse(); 221 | 222 | $client->appdata["path"] = false; 223 | } 224 | } 225 | } 226 | else 227 | { 228 | // Handle WebRoute upgrade requests. 229 | $ipaddr = $client->ipaddr; 230 | $id2 = (isset($pathmap[$client->appdata["path"]]) ? $wrserver->ProcessWebServerClientUpgrade($webserver, $client) : false); 231 | if ($id2 !== false) 232 | { 233 | $client2 = $wrserver->GetClient($id2); 234 | 235 | echo "Webserver client ID " . $id . " upgraded to WebRoute. WebRoute client ID is " . $id2 . ".\n"; 236 | 237 | // Notify the appropriate WebSocket server. 238 | $id3 = $pathmap[$client2->appdata["path"]]; 239 | $client3 = $wsserver->GetClient($id3); 240 | 241 | $data = array( 242 | "ipaddr" => $ipaddr, 243 | "id" => $client2->webrouteid, 244 | "timeout" => (isset($client2->headers["Webroute-Timeout"]) && is_numeric($client2->headers["Webroute-Timeout"]) && (int)$client2->headers["Webroute-Timeout"] > $client2->timeout ? (int)$client2->headers["Webroute-Timeout"] : $client2->timeout) 245 | ); 246 | 247 | $client3->websocket->Write(json_encode($data, JSON_UNESCAPED_SLASHES), WebSocket::FRAMETYPE_TEXT); 248 | 249 | $client3->appdata["waiting"][$id2] = true; 250 | } 251 | else 252 | { 253 | if (!isset($pathmap[$client->appdata["path"]])) 254 | { 255 | $result2 = array("success" => false, "error" => "Requested destination does not exist at this time.", "errorcode" => "missing_destination"); 256 | 257 | $client->SetResponseCode(502); 258 | } 259 | else 260 | { 261 | $result2 = array("success" => false, "error" => "Invalid request. Expected WebRoute upgrade.", "errorcode" => "invalid_request"); 262 | 263 | $client->SetResponseCode(400); 264 | } 265 | 266 | // Send the response. 267 | $client->SetResponseContentType("application/json"); 268 | $client->AddResponseContent(json_encode($result2)); 269 | $client->FinalizeResponse(); 270 | 271 | $client->appdata["path"] = false; 272 | } 273 | } 274 | } 275 | } 276 | 277 | // Handle removed clients. 278 | foreach ($result["removed"] as $id => $result2) 279 | { 280 | if ($result2["client"]->appdata !== false) 281 | { 282 | echo "Web server client ID " . $id . " disconnected.\n"; 283 | 284 | // echo "Client ID " . $id . " disconnected. Reason:\n"; 285 | // var_dump($result2["result"]); 286 | // echo "\n"; 287 | } 288 | } 289 | 290 | // WebSocket server. 291 | $result = $wsserver->Wait(0); 292 | 293 | // Handle active clients. 294 | foreach ($result["clients"] as $id => $client) 295 | { 296 | // Ignore all input packets. 297 | $ws = $client->websocket; 298 | 299 | $result2 = $ws->Read(); 300 | while ($result2["success"] && $result2["data"] !== false) 301 | { 302 | $result2 = $ws->Read(); 303 | } 304 | } 305 | 306 | foreach ($result["removed"] as $id => $result2) 307 | { 308 | $client = $result2["client"]; 309 | 310 | if ($client->appdata !== false) 311 | { 312 | echo "WebSocket client ID " . $id . " disconnected.\n"; 313 | 314 | // Remove the path. 315 | unset($pathmap[$client->appdata["path"]]); 316 | 317 | // Disconnect waiting WebRoute clients so they don't timeout. 318 | foreach ($client->appdata["waiting"] as $id2 => $val) 319 | { 320 | $wrserver->RemoveClient($id2); 321 | } 322 | 323 | // echo "WebSocket client ID " . $id . " disconnected. Reason:\n"; 324 | // var_dump($result2["result"]); 325 | // echo "\n"; 326 | } 327 | } 328 | 329 | // WebRoute server. 330 | $result = $wrserver->Wait(0); 331 | 332 | foreach ($result["removed"] as $id => $result2) 333 | { 334 | $client = $result2["client"]; 335 | 336 | if ($client->appdata !== false) 337 | { 338 | echo "WebRoute client ID " . $id . " disconnected.\n"; 339 | 340 | // Remove from the associated WebSocket waiting queue. 341 | if (isset($pathmap[$client->appdata["path"]])) 342 | { 343 | $id2 = $pathmap[$client->appdata["path"]]; 344 | $client2 = $wsserver->GetClient($id2); 345 | 346 | unset($client2->appdata["waiting"][$id]); 347 | } 348 | 349 | // echo "WebRoute client ID " . $id . " disconnected. Reason:\n"; 350 | // var_dump($result2["result"]); 351 | // echo "\n"; 352 | } 353 | } 354 | 355 | // Check the status of the two service file options for correct Service Manager integration. 356 | if ($lastservicecheck <= time() - 3) 357 | { 358 | if (file_exists($stopfilename)) 359 | { 360 | // Initialize termination. 361 | echo "Stop requested.\n"; 362 | 363 | $running = false; 364 | } 365 | else if (file_exists($reloadfilename)) 366 | { 367 | // Reload configuration and then remove reload file. 368 | echo "Reload config requested. Exiting.\n"; 369 | 370 | $running = false; 371 | } 372 | 373 | $lastservicecheck = time(); 374 | } 375 | } while ($running); 376 | ?> -------------------------------------------------------------------------------- /support/cli.php: -------------------------------------------------------------------------------- 1 | $val) 15 | { 16 | if (!isset($options["rules"][$val])) unset($options["shortmap"][$key]); 17 | } 18 | foreach ($options["rules"] as $key => $val) 19 | { 20 | if (!isset($val["arg"])) $options["rules"][$key]["arg"] = false; 21 | if (!isset($val["multiple"])) $options["rules"][$key]["multiple"] = false; 22 | } 23 | 24 | if ($args === false) $args = $_SERVER["argv"]; 25 | else if (is_string($args)) 26 | { 27 | $args2 = $args; 28 | $args = array(); 29 | $inside = false; 30 | $currarg = ""; 31 | $y = strlen($args2); 32 | for ($x = 0; $x < $y; $x++) 33 | { 34 | $currchr = substr($args2, $x, 1); 35 | 36 | if ($inside === false && $currchr == " " && $currarg != "") 37 | { 38 | $args[] = $currarg; 39 | $currarg = ""; 40 | } 41 | else if ($currchr == "\"" || $currchr == "'") 42 | { 43 | if ($inside === false) $inside = $currchr; 44 | else if ($inside === $currchr) $inside = false; 45 | else $currarg .= $currchr; 46 | } 47 | else if ($currchr == "\\" && $x < $y - 1) 48 | { 49 | $x++; 50 | $currarg .= substr($args2, $x, 1); 51 | } 52 | else if ($inside !== false || $currchr != " ") 53 | { 54 | $currarg .= $currchr; 55 | } 56 | } 57 | 58 | if ($currarg != "") $args[] = $currarg; 59 | } 60 | 61 | $result = array("success" => true, "file" => array_shift($args), "opts" => array(), "params" => array()); 62 | 63 | // Look over shortmap to determine if options exist that are one byte (flags) and don't have arguments. 64 | $chrs = array(); 65 | foreach ($options["shortmap"] as $key => $val) 66 | { 67 | if (isset($options["rules"][$val]) && !$options["rules"][$val]["arg"]) $chrs[$key] = true; 68 | } 69 | 70 | $allowopt = true; 71 | $y = count($args); 72 | for ($x = 0; $x < $y; $x++) 73 | { 74 | $arg = $args[$x]; 75 | 76 | // Attempt to process an option. 77 | $opt = false; 78 | $optval = false; 79 | if ($allowopt && substr($arg, 0, 1) == "-") 80 | { 81 | $pos = strpos($arg, "="); 82 | if ($pos === false) $pos = strlen($arg); 83 | else $optval = substr($arg, $pos + 1); 84 | $arg2 = substr($arg, 1, $pos - 1); 85 | 86 | if (isset($options["rules"][$arg2])) $opt = $arg2; 87 | else if (isset($options["shortmap"][$arg2])) $opt = $options["shortmap"][$arg2]; 88 | else if ($x == 0) 89 | { 90 | // Attempt to process as a set of flags. 91 | $y2 = strlen($arg2); 92 | if ($y2 > 0) 93 | { 94 | for ($x2 = 0; $x2 < $y2; $x2++) 95 | { 96 | $currchr = substr($arg2, $x2, 1); 97 | 98 | if (!isset($chrs[$currchr])) break; 99 | } 100 | 101 | if ($x2 == $y2) 102 | { 103 | for ($x2 = 0; $x2 < $y2; $x2++) 104 | { 105 | $opt = $options["shortmap"][substr($arg2, $x2, 1)]; 106 | 107 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = true; 108 | else 109 | { 110 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = 0; 111 | $result["opts"][$opt]++; 112 | } 113 | } 114 | 115 | continue; 116 | } 117 | } 118 | } 119 | } 120 | 121 | if ($opt === false) 122 | { 123 | // Is a parameter. 124 | if (substr($arg, 0, 1) === "\"" || substr($arg, 0, 1) === "'") $arg = substr($arg, 1); 125 | if (substr($arg, -1) === "\"" || substr($arg, -1) === "'") $arg = substr($arg, 0, -1); 126 | 127 | $result["params"][] = $arg; 128 | 129 | if (!$options["allow_opts_after_param"]) $allowopt = false; 130 | } 131 | else if (!$options["rules"][$opt]["arg"]) 132 | { 133 | // Is a flag by itself. 134 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = true; 135 | else 136 | { 137 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = 0; 138 | $result["opts"][$opt]++; 139 | } 140 | } 141 | else 142 | { 143 | // Is an option. 144 | if ($optval === false) 145 | { 146 | $x++; 147 | if ($x == $y) break; 148 | $optval = $args[$x]; 149 | } 150 | 151 | if (substr($optval, 0, 1) === "\"" || substr($optval, 0, 1) === "'") $optval = substr($optval, 1); 152 | if (substr($optval, -1) === "\"" || substr($optval, -1) === "'") $optval = substr($optval, 0, -1); 153 | 154 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = $optval; 155 | else 156 | { 157 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = array(); 158 | $result["opts"][$opt][] = $optval; 159 | } 160 | } 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | public static function CanGetUserInputWithArgs(&$args, $prefix) 167 | { 168 | return (($prefix !== false && isset($args["opts"][$prefix]) && is_array($args["opts"][$prefix]) && count($args["opts"][$prefix])) || count($args["params"])); 169 | } 170 | 171 | // Gets a line of input from the user. If the user supplies all information via the command-line, this could be entirely automated. 172 | public static function GetUserInputWithArgs(&$args, $prefix, $question, $default, $noparamsoutput = "", $suppressoutput = false, $callback = false, $callbackopts = false) 173 | { 174 | if (!self::CanGetUserInputWithArgs($args, $prefix) && $noparamsoutput != "") 175 | { 176 | echo "\n" . rtrim($noparamsoutput) . "\n\n"; 177 | 178 | $suppressoutput = false; 179 | $noparamsoutput = ""; 180 | } 181 | 182 | do 183 | { 184 | $prompt = ($suppressoutput ? "" : $question . ($default !== false ? " [" . $default . "]" : "") . ": "); 185 | 186 | if ($prefix !== false && isset($args["opts"][$prefix]) && is_array($args["opts"][$prefix]) && count($args["opts"][$prefix])) 187 | { 188 | $line = array_shift($args["opts"][$prefix]); 189 | if ($line === "") $line = $default; 190 | if (!$suppressoutput) echo $prompt . $line . "\n"; 191 | } 192 | else if (count($args["params"])) 193 | { 194 | $line = array_shift($args["params"]); 195 | if ($line === "") $line = $default; 196 | if (!$suppressoutput) echo $prompt . $line . "\n"; 197 | } 198 | else if (strtoupper(substr(php_uname("s"), 0, 3)) != "WIN" && function_exists("readline") && function_exists("readline_add_history")) 199 | { 200 | $line = readline($prompt); 201 | if ($line === false) exit(); 202 | 203 | $line = trim($line); 204 | if ($line === "") $line = $default; 205 | if ($line !== false && $line !== "") readline_add_history($line); 206 | } 207 | else 208 | { 209 | echo $prompt; 210 | fflush(STDOUT); 211 | $line = fgets(STDIN); 212 | if ($line === false || ($line === "" && feof(STDIN))) exit(); 213 | 214 | $line = trim($line); 215 | if ($line === "") $line = $default; 216 | } 217 | 218 | if ($line === false || (is_callable($callback) && !call_user_func_array($callback, array($line, &$callbackopts)))) 219 | { 220 | if ($line !== false) $line = false; 221 | else echo "Please enter a value.\n"; 222 | 223 | if (!self::CanGetUserInputWithArgs($args, $prefix) && $noparamsoutput != "") 224 | { 225 | echo "\n" . $noparamsoutput . "\n"; 226 | 227 | $noparamsoutput = ""; 228 | } 229 | 230 | $suppressoutput = false; 231 | } 232 | } while ($line === false); 233 | 234 | return $line; 235 | } 236 | 237 | // Obtains a valid line of input. 238 | public static function GetLimitedUserInputWithArgs(&$args, $prefix, $question, $default, $allowedoptionsprefix, $allowedoptions, $loop = true, $suppressoutput = false, $multipleuntil = false) 239 | { 240 | $noparamsoutput = $allowedoptionsprefix . "\n\n"; 241 | $size = 0; 242 | foreach ($allowedoptions as $key => $val) 243 | { 244 | if ($size < strlen($key)) $size = strlen($key); 245 | } 246 | 247 | foreach ($allowedoptions as $key => $val) 248 | { 249 | $newtab = str_repeat(" ", 2 + $size + 3); 250 | $noparamsoutput .= " " . $key . ":" . str_repeat(" ", $size - strlen($key)) . " " . str_replace("\n\t", "\n" . $newtab, $val) . "\n"; 251 | } 252 | 253 | $noparamsoutput .= "\n"; 254 | 255 | if ($default === false && count($allowedoptions) == 1) 256 | { 257 | reset($allowedoptions); 258 | $default = key($allowedoptions); 259 | } 260 | 261 | $results = array(); 262 | do 263 | { 264 | $displayed = (!count($args["params"])); 265 | $result = self::GetUserInputWithArgs($args, $prefix, $question, $default, $noparamsoutput, $suppressoutput); 266 | if (is_array($multipleuntil) && $multipleuntil["exit"] === $result) break; 267 | $result2 = false; 268 | if (!count($allowedoptions)) break; 269 | foreach ($allowedoptions as $key => $val) 270 | { 271 | if (!strcasecmp($key, $result) || !strcasecmp($val, $result)) $result2 = $key; 272 | } 273 | if ($loop) 274 | { 275 | if ($result2 === false) 276 | { 277 | echo "Please select an option from the list.\n"; 278 | 279 | $suppressoutput = false; 280 | } 281 | else if (is_array($multipleuntil)) 282 | { 283 | $results[$result2] = $result2; 284 | 285 | $question = $multipleuntil["nextquestion"]; 286 | $default = $multipleuntil["nextdefault"]; 287 | } 288 | } 289 | 290 | if ($displayed) $noparamsoutput = ""; 291 | } while ($loop && ($result2 === false || is_array($multipleuntil))); 292 | 293 | return (is_array($multipleuntil) ? $results : $result2); 294 | } 295 | 296 | // Obtains Yes/No style input. 297 | public static function GetYesNoUserInputWithArgs(&$args, $prefix, $question, $default, $noparamsoutput = "", $suppressoutput = false) 298 | { 299 | $default = (substr(strtoupper(trim($default)), 0, 1) === "Y" ? "Y" : "N"); 300 | 301 | $result = self::GetUserInputWithArgs($args, $prefix, $question, $default, $noparamsoutput, $suppressoutput); 302 | $result = (substr(strtoupper(trim($result)), 0, 1) === "Y"); 303 | 304 | return $result; 305 | } 306 | 307 | public static function GetHexDump($data) 308 | { 309 | $result = ""; 310 | 311 | $x = 0; 312 | $y = strlen($data); 313 | if ($y <= 256) $padwidth = 2; 314 | else if ($y <= 65536) $padwidth = 4; 315 | else if ($y <= 16777216) $padwidth = 6; 316 | else $padwidth = 8; 317 | 318 | $pad = str_repeat(" ", $padwidth); 319 | 320 | $data2 = str_split(strtoupper(bin2hex($data)), 32); 321 | foreach ($data2 as $line) 322 | { 323 | $result .= sprintf("%0" . $padwidth . "X", $x) . " | "; 324 | 325 | $line = str_split($line, 2); 326 | array_splice($line, 8, 0, ""); 327 | $result .= implode(" ", $line) . "\n"; 328 | 329 | $result .= $pad . " |"; 330 | $y2 = $x + 16; 331 | for ($x2 = 0; $x2 < 16 && $x < $y; $x2++) 332 | { 333 | $result .= " "; 334 | if ($x2 === 8) $result .= " "; 335 | 336 | $tempchr = ord($data[$x]); 337 | if ($tempchr === 0x09) $result .= "\\t"; 338 | else if ($tempchr === 0x0D) $result .= "\\r"; 339 | else if ($tempchr === 0x0A) $result .= "\\n"; 340 | else if ($tempchr === 0x00) $result .= "\\0"; 341 | else if ($tempchr < 32 || $tempchr > 126) $result .= " "; 342 | else $result .= " " . $data[$x]; 343 | 344 | $x++; 345 | } 346 | 347 | $result .= "\n"; 348 | } 349 | 350 | return $result; 351 | } 352 | 353 | // Outputs a JSON array (useful for captured output). 354 | public static function DisplayResult($result, $exit = true) 355 | { 356 | if (is_array($result)) echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 357 | else echo $result . "\n"; 358 | 359 | if ($exit) exit(); 360 | } 361 | 362 | // Useful for reparsing remaining parameters as new arguments. 363 | public static function ReinitArgs(&$args, $newargs) 364 | { 365 | // Process the parameters. 366 | $options = array( 367 | "shortmap" => array( 368 | "?" => "help" 369 | ), 370 | "rules" => array( 371 | ) 372 | ); 373 | 374 | foreach ($newargs as $arg) $options["rules"][$arg] = array("arg" => true, "multiple" => true); 375 | $options["rules"]["help"] = array("arg" => false); 376 | 377 | $args = self::ParseCommandLine($options, array_merge(array(""), $args["params"])); 378 | 379 | if (isset($args["opts"]["help"])) self::DisplayResult(array("success" => true, "options" => array_keys($options["rules"]))); 380 | } 381 | 382 | // Tracks messages for a command-line interface app. 383 | private static $messages = array(); 384 | 385 | public static function LogMessage($msg, $data = null) 386 | { 387 | if (isset($data)) $msg .= "\n\t" . trim(str_replace("\n", "\n\t", json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))); 388 | 389 | self::$messages[] = $msg; 390 | 391 | fwrite(STDERR, $msg . "\n"); 392 | } 393 | 394 | public static function DisplayError($msg, $result = false, $exit = true) 395 | { 396 | self::LogMessage(($exit ? "[Error] " : "") . $msg); 397 | 398 | if ($result !== false && is_array($result) && isset($result["error"]) && isset($result["errorcode"])) self::LogMessage("[Error] " . $result["error"] . " (" . $result["errorcode"] . ")", (isset($result["info"]) ? $result["info"] : null)); 399 | 400 | if ($exit) exit(); 401 | } 402 | 403 | public static function GetLogMessages($filters = array()) 404 | { 405 | if (is_string($filters)) $filters = array($filters); 406 | 407 | $result = array(); 408 | foreach (self::$messages as $message) 409 | { 410 | $found = (!count($filters)); 411 | foreach ($filters as $filter) 412 | { 413 | if (preg_match($filter, $message)) $found = true; 414 | } 415 | 416 | if ($found) $result[] = $message; 417 | } 418 | 419 | return $result; 420 | } 421 | 422 | public static function ResetLogMessages() 423 | { 424 | self::$messages = array(); 425 | } 426 | 427 | 428 | private static $timerinfo = array(); 429 | 430 | public static function StartTimer() 431 | { 432 | $ts = microtime(true); 433 | 434 | self::$timerinfo = array( 435 | "start" => $ts, 436 | "diff" => $ts 437 | ); 438 | } 439 | 440 | public static function UpdateTimer() 441 | { 442 | $ts = microtime(true); 443 | $diff = $ts - self::$timerinfo["diff"]; 444 | self::$timerinfo["diff"] = $ts; 445 | 446 | $result = array( 447 | "success" => true, 448 | "diff" => sprintf("%.2f", $diff), 449 | "total" => sprintf("%.2f", $ts - self::$timerinfo["start"]) 450 | ); 451 | 452 | return $result; 453 | } 454 | } 455 | ?> -------------------------------------------------------------------------------- /support/utf_utils.php: -------------------------------------------------------------------------------- 1 | = 0x0300 && $val <= 0x036F) || ($val >= 0x1DC0 && $val <= 0x1DFF) || ($val >= 0x20D0 && $val <= 0x20FF) || ($val >= 0xFE20 && $val <= 0xFE2F)); 21 | } 22 | 23 | public static function Convert($data, $srctype, $desttype) 24 | { 25 | $arr = is_array($data); 26 | if ($arr) $srctype = self::UTF32_ARRAY; 27 | $x = 0; 28 | $y = ($arr ? count($data) : strlen($data)); 29 | $result = ($desttype === self::UTF32_ARRAY ? array() : ""); 30 | if (!$arr && $srctype === self::UTF32_ARRAY) return $result; 31 | 32 | $first = true; 33 | 34 | if ($srctype === self::UTF8_BOM) 35 | { 36 | if (substr($data, 0, 3) === "\xEF\xBB\xBF") $x = 3; 37 | 38 | $srctype = self::UTF8; 39 | } 40 | 41 | if ($srctype === self::UTF16_BOM) 42 | { 43 | if (substr($data, 0, 2) === "\xFE\xFF") 44 | { 45 | $srctype = self::UTF16_BE; 46 | $x = 2; 47 | } 48 | else if (substr($data, 0, 2) === "\xFF\xFE") 49 | { 50 | $srctype = self::UTF16_LE; 51 | $x = 2; 52 | } 53 | else 54 | { 55 | $srctype = self::UTF16_LE; 56 | } 57 | } 58 | 59 | if ($srctype === self::UTF32_BOM) 60 | { 61 | if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") 62 | { 63 | $srctype = self::UTF32_BE; 64 | $x = 4; 65 | } 66 | else if (substr($data, 0, 4) === "\xFF\xFE\x00\x00") 67 | { 68 | $srctype = self::UTF32_LE; 69 | $x = 4; 70 | } 71 | else 72 | { 73 | $srctype = self::UTF32_LE; 74 | } 75 | } 76 | 77 | while ($x < $y) 78 | { 79 | // Read the next valid code point. 80 | $val = false; 81 | 82 | switch ($srctype) 83 | { 84 | case self::UTF8: 85 | { 86 | $tempchr = ord($data[$x]); 87 | if ($tempchr <= 0x7F) 88 | { 89 | $val = $tempchr; 90 | $x++; 91 | } 92 | else if ($tempchr < 0xC2) $x++; 93 | else 94 | { 95 | $left = $y - $x; 96 | if ($left < 2) $x++; 97 | else 98 | { 99 | $tempchr2 = ord($data[$x + 1]); 100 | 101 | if (($tempchr >= 0xC2 && $tempchr <= 0xDF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) 102 | { 103 | $val = (($tempchr & 0x1F) << 6) | ($tempchr2 & 0x3F); 104 | $x += 2; 105 | } 106 | else if ($left < 3) $x++; 107 | else 108 | { 109 | $tempchr3 = ord($data[$x + 2]); 110 | 111 | if ($tempchr3 < 0x80 || $tempchr3 > 0xBF) $x++; 112 | else 113 | { 114 | if (($tempchr == 0xE0 && ($tempchr2 >= 0xA0 && $tempchr2 <= 0xBF)) || ((($tempchr >= 0xE1 && $tempchr <= 0xEC) || $tempchr == 0xEE || $tempchr == 0xEF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xED && ($tempchr2 >= 0x80 && $tempchr2 <= 0x9F))) 115 | { 116 | $val = (($tempchr & 0x0F) << 12) | (($tempchr2 & 0x3F) << 6) | ($tempchr3 & 0x3F); 117 | $x += 3; 118 | } 119 | else if ($left < 4) $x++; 120 | else 121 | { 122 | $tempchr4 = ord($data[$x + 3]); 123 | 124 | if ($tempchr4 < 0x80 || $tempchr4 > 0xBF) $x++; 125 | else if (($tempchr == 0xF0 && ($tempchr2 >= 0x90 && $tempchr2 <= 0xBF)) || (($tempchr >= 0xF1 && $tempchr <= 0xF3) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xF4 && ($tempchr2 >= 0x80 && $tempchr2 <= 0x8F))) 126 | { 127 | $val = (($tempchr & 0x07) << 18) | (($tempchr2 & 0x3F) << 12) | (($tempchr3 & 0x3F) << 6) | ($tempchr4 & 0x3F); 128 | $x += 4; 129 | } 130 | else 131 | { 132 | $x++; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | break; 141 | } 142 | case self::UTF16_LE: 143 | { 144 | if ($x + 1 >= $y) $x = $y; 145 | else 146 | { 147 | $val = unpack("v", substr($data, $x, 2))[1]; 148 | $x += 2; 149 | 150 | if ($val >= 0xD800 && $val <= 0xDBFF) 151 | { 152 | if ($x + 1 >= $y) 153 | { 154 | $x = $y; 155 | $val = false; 156 | } 157 | else 158 | { 159 | $val2 = unpack("v", substr($data, $x, 2))[1]; 160 | 161 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 162 | else 163 | { 164 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 165 | $x += 2; 166 | } 167 | } 168 | } 169 | } 170 | 171 | break; 172 | } 173 | case self::UTF16_BE: 174 | { 175 | if ($x + 1 >= $y) $x = $y; 176 | else 177 | { 178 | $val = unpack("n", substr($data, $x, 2))[1]; 179 | $x += 2; 180 | 181 | if ($val >= 0xD800 && $val <= 0xDBFF) 182 | { 183 | if ($x + 1 >= $y) 184 | { 185 | $x = $y; 186 | $val = false; 187 | } 188 | else 189 | { 190 | $val2 = unpack("n", substr($data, $x, 2))[1]; 191 | 192 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 193 | else 194 | { 195 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 196 | $x += 2; 197 | } 198 | } 199 | } 200 | } 201 | 202 | break; 203 | } 204 | case self::UTF32_LE: 205 | { 206 | if ($x + 3 >= $y) $x = $y; 207 | else 208 | { 209 | $val = unpack("V", substr($data, $x, 4))[1]; 210 | $x += 4; 211 | } 212 | 213 | break; 214 | } 215 | case self::UTF32_BE: 216 | { 217 | if ($x + 3 >= $y) $x = $y; 218 | else 219 | { 220 | $val = unpack("N", substr($data, $x, 4))[1]; 221 | $x += 4; 222 | } 223 | 224 | break; 225 | } 226 | case self::UTF32_ARRAY: 227 | { 228 | $val = (int)$data[$x]; 229 | $x++; 230 | 231 | break; 232 | } 233 | default: $x = $y; break; 234 | } 235 | 236 | // Make sure it is a valid Unicode value. 237 | // 0xD800-0xDFFF are for UTF-16 surrogate pairs. Invalid characters. 238 | // 0xFDD0-0xFDEF are non-characters. 239 | // 0x*FFFE and 0x*FFFF are reserved. 240 | // The largest possible character is 0x10FFFF. 241 | // First character can't be a combining code point. 242 | if ($val !== false && !($val < 0 || ($val >= 0xD800 && $val <= 0xDFFF) || ($val >= 0xFDD0 && $val <= 0xFDEF) || ($val & 0xFFFE) == 0xFFFE || $val > 0x10FFFF || ($first && self::IsCombiningCodePoint($val)))) 243 | { 244 | if ($first) 245 | { 246 | if ($desttype === self::UTF8_BOM) 247 | { 248 | $result .= "\xEF\xBB\xBF"; 249 | 250 | $desttype = self::UTF8; 251 | } 252 | 253 | if ($desttype === self::UTF16_BOM) 254 | { 255 | $result .= "\xFF\xFE"; 256 | 257 | $desttype = self::UTF16_LE; 258 | } 259 | 260 | if ($srctype === self::UTF32_BOM) 261 | { 262 | $result .= "\xFF\xFE\x00\x00"; 263 | 264 | $desttype = self::UTF32_LE; 265 | } 266 | 267 | $first = false; 268 | } 269 | 270 | switch ($desttype) 271 | { 272 | case self::UTF8: 273 | { 274 | if ($val <= 0x7F) $result .= chr($val); 275 | else if ($val <= 0x7FF) $result .= chr(0xC0 | ($val >> 6)) . chr(0x80 | ($val & 0x3F)); 276 | else if ($val <= 0xFFFF) $result .= chr(0xE0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 277 | else if ($val <= 0x10FFFF) $result .= chr(0xF0 | ($val >> 18)) . chr(0x80 | (($val >> 12) & 0x3F)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 278 | 279 | break; 280 | } 281 | case self::UTF16_LE: 282 | { 283 | if ($val <= 0xFFFF) $result .= pack("v", $val); 284 | else 285 | { 286 | $val -= 0x10000; 287 | $result .= pack("v", ((($val >> 10) & 0x3FF) + 0xD800)); 288 | $result .= pack("v", (($val & 0x3FF) + 0xDC00)); 289 | } 290 | 291 | break; 292 | } 293 | case self::UTF16_BE: 294 | { 295 | if ($val <= 0xFFFF) $result .= pack("n", $val); 296 | else 297 | { 298 | $val -= 0x10000; 299 | $result .= pack("n", ((($val >> 10) & 0x3FF) + 0xD800)); 300 | $result .= pack("n", (($val & 0x3FF) + 0xDC00)); 301 | } 302 | 303 | break; 304 | } 305 | case self::UTF32_LE: 306 | { 307 | $result .= pack("V", $val); 308 | 309 | break; 310 | } 311 | case self::UTF32_BE: 312 | { 313 | $result .= pack("N", $val); 314 | 315 | break; 316 | } 317 | case self::UTF32_ARRAY: 318 | { 319 | $result[] = $val; 320 | 321 | break; 322 | } 323 | default: $x = $y; break; 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | 332 | protected const PUNYCODE_BASE = 36; 333 | protected const PUNYCODE_TMIN = 1; 334 | protected const PUNYCODE_TMAX = 26; 335 | protected const PUNYCODE_SKEW = 38; 336 | protected const PUNYCODE_DAMP = 700; 337 | protected const PUNYCODE_INITIAL_BIAS = 72; 338 | protected const PUNYCODE_INITIAL_N = 0x80; 339 | protected const PUNYCODE_DIGIT_MAP = "abcdefghijklmnopqrstuvwxyz0123456789"; 340 | 341 | public static function ConvertToPunycode($domain) 342 | { 343 | // Reject invalid domain name lengths. 344 | if (strlen($domain) > 255) return false; 345 | 346 | $parts = explode(".", $domain); 347 | 348 | foreach ($parts as $num => $part) 349 | { 350 | // Reject invalid label lengths. 351 | $y = strlen($part); 352 | if ($y > 63) return false; 353 | 354 | // Skip already encoded portions. 355 | if (substr($part, 0, 4) === "xn--") continue; 356 | 357 | // Convert UTF-8 to UTF-32 code points. 358 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 359 | 360 | // Handle ASCII code points. 361 | $part2 = ""; 362 | foreach ($data as $cp) 363 | { 364 | if ($cp <= 0x7F) $part2 .= strtolower(chr($cp)); 365 | } 366 | 367 | $numhandled = strlen($part2); 368 | $y = count($data); 369 | 370 | if ($numhandled >= $y) 371 | { 372 | $parts[$num] = $part2; 373 | 374 | continue; 375 | } 376 | 377 | if ($numhandled) $part2 .= "-"; 378 | 379 | $part2 = "xn--" . $part2; 380 | 381 | if (strlen($part2) > 63) return false; 382 | 383 | $bias = self::PUNYCODE_INITIAL_BIAS; 384 | $n = self::PUNYCODE_INITIAL_N; 385 | $delta = 0; 386 | $first = true; 387 | 388 | while ($numhandled < $y) 389 | { 390 | // Find the next largest unhandled code point. 391 | $cp2 = 0x01000000; 392 | foreach ($data as $cp) 393 | { 394 | if ($cp >= $n && $cp2 > $cp) $cp2 = $cp; 395 | } 396 | 397 | // Increase delta but prevent overflow. 398 | $delta += ($cp2 - $n) * ($numhandled + 1); 399 | if ($delta < 0) return false; 400 | $n = $cp2; 401 | 402 | foreach ($data as $cp) 403 | { 404 | if ($cp < $n) 405 | { 406 | $delta++; 407 | 408 | if ($delta < 0) return false; 409 | } 410 | else if ($cp === $n) 411 | { 412 | // Calculate and encode a variable length integer from the delta. 413 | $q = $delta; 414 | $x = 0; 415 | do 416 | { 417 | $x += self::PUNYCODE_BASE; 418 | 419 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 420 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 421 | else $t = $x - $bias; 422 | 423 | if ($q < $t) break; 424 | 425 | $part2 .= self::PUNYCODE_DIGIT_MAP[$t + (($q - $t) % (self::PUNYCODE_BASE - $t))]; 426 | 427 | $q = (int)(($q - $t) / (self::PUNYCODE_BASE - $t)); 428 | 429 | if (strlen($part2) > 63) return false; 430 | } while (1); 431 | 432 | $part2 .= self::PUNYCODE_DIGIT_MAP[$q]; 433 | if (strlen($part2) > 63) return false; 434 | 435 | // Adapt bias. 436 | $numhandled++; 437 | $bias = self::InternalPunycodeAdapt($delta, $numhandled, $first); 438 | $delta = 0; 439 | $first = false; 440 | } 441 | } 442 | 443 | $delta++; 444 | $n++; 445 | } 446 | 447 | $parts[$num] = $part2; 448 | } 449 | 450 | return implode(".", $parts); 451 | } 452 | 453 | public static function ConvertFromPunycode($domain) 454 | { 455 | // Reject invalid domain name lengths. 456 | if (strlen($domain) > 255) return false; 457 | 458 | $parts = explode(".", $domain); 459 | 460 | foreach ($parts as $num => $part) 461 | { 462 | // Reject invalid label lengths. 463 | $y = strlen($part); 464 | if ($y > 63) return false; 465 | 466 | // Skip unencoded portions. 467 | if (substr($part, 0, 4) !== "xn--") continue; 468 | 469 | $part = substr($part, 4); 470 | 471 | // Convert UTF-8 to UTF-32 code points. 472 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 473 | 474 | // Handle ASCII code points. 475 | $hyphen = ord("-"); 476 | for ($x = count($data); $x && $data[$x - 1] !== $hyphen; $x--); 477 | if (!$x) $data2 = array(); 478 | else 479 | { 480 | $data2 = array_splice($data, 0, $x - 1); 481 | 482 | array_shift($data); 483 | } 484 | 485 | $numhandled = count($data2); 486 | 487 | $bias = self::PUNYCODE_INITIAL_BIAS; 488 | $n = self::PUNYCODE_INITIAL_N; 489 | $delta = 0; 490 | $first = true; 491 | 492 | $pos = 0; 493 | $y = count($data); 494 | while ($pos < $y) 495 | { 496 | // Calculate and decode a delta from the variable length integer. 497 | $olddelta = $delta; 498 | $w = 1; 499 | $x = 0; 500 | do 501 | { 502 | $x += self::PUNYCODE_BASE; 503 | 504 | $cp = $data[$pos]; 505 | $pos++; 506 | 507 | if ($cp >= ord("a") && $cp <= ord("z")) $digit = $cp - ord("a"); 508 | else if ($cp >= ord("A") && $cp <= ord("Z")) $digit = $cp - ord("A"); 509 | else if ($cp >= ord("0") && $cp <= ord("9")) $digit = $cp - ord("0") + 26; 510 | else return false; 511 | 512 | $delta += $digit * $w; 513 | if ($delta < 0) return false; 514 | 515 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 516 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 517 | else $t = $x - $bias; 518 | 519 | if ($digit < $t) break; 520 | 521 | $w *= (self::PUNYCODE_BASE - $t); 522 | if ($w < 0) return false; 523 | } while (1); 524 | 525 | // Adapt bias. 526 | $numhandled++; 527 | $bias = self::InternalPunycodeAdapt($delta - $olddelta, $numhandled, $first); 528 | $first = false; 529 | 530 | // Delta was supposed to wrap around from $numhandled to 0, incrementing $n each time, so fix that now. 531 | $n += (int)($delta / $numhandled); 532 | $delta %= $numhandled; 533 | 534 | // Insert $n (the code point) at the delta position. 535 | array_splice($data2, $delta, 0, array($n)); 536 | $delta++; 537 | } 538 | 539 | $parts[$num] = self::Convert($data2, self::UTF32_ARRAY, self::UTF8); 540 | } 541 | 542 | return implode(".", $parts); 543 | } 544 | 545 | // RFC3492 adapt() function. 546 | protected static function InternalPunycodeAdapt($delta, $numpoints, $first) 547 | { 548 | $delta = ($first ? (int)($delta / self::PUNYCODE_DAMP) : $delta >> 1); 549 | $delta += (int)($delta / $numpoints); 550 | 551 | $y = self::PUNYCODE_BASE - self::PUNYCODE_TMIN; 552 | 553 | $condval = (int)(($y * self::PUNYCODE_TMAX) / 2); 554 | for ($x = 0; $delta > $condval; $x += self::PUNYCODE_BASE) $delta = (int)($delta / $y); 555 | 556 | return (int)($x + ((($y + 1) * $delta) / ($delta + self::PUNYCODE_SKEW))); 557 | } 558 | } 559 | ?> -------------------------------------------------------------------------------- /sdks/php/utf_utils.php: -------------------------------------------------------------------------------- 1 | = 0x0300 && $val <= 0x036F) || ($val >= 0x1DC0 && $val <= 0x1DFF) || ($val >= 0x20D0 && $val <= 0x20FF) || ($val >= 0xFE20 && $val <= 0xFE2F)); 21 | } 22 | 23 | public static function Convert($data, $srctype, $desttype) 24 | { 25 | $arr = is_array($data); 26 | if ($arr) $srctype = self::UTF32_ARRAY; 27 | $x = 0; 28 | $y = ($arr ? count($data) : strlen($data)); 29 | $result = ($desttype === self::UTF32_ARRAY ? array() : ""); 30 | if (!$arr && $srctype === self::UTF32_ARRAY) return $result; 31 | 32 | $first = true; 33 | 34 | if ($srctype === self::UTF8_BOM) 35 | { 36 | if (substr($data, 0, 3) === "\xEF\xBB\xBF") $x = 3; 37 | 38 | $srctype = self::UTF8; 39 | } 40 | 41 | if ($srctype === self::UTF16_BOM) 42 | { 43 | if (substr($data, 0, 2) === "\xFE\xFF") 44 | { 45 | $srctype = self::UTF16_BE; 46 | $x = 2; 47 | } 48 | else if (substr($data, 0, 2) === "\xFF\xFE") 49 | { 50 | $srctype = self::UTF16_LE; 51 | $x = 2; 52 | } 53 | else 54 | { 55 | $srctype = self::UTF16_LE; 56 | } 57 | } 58 | 59 | if ($srctype === self::UTF32_BOM) 60 | { 61 | if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") 62 | { 63 | $srctype = self::UTF32_BE; 64 | $x = 4; 65 | } 66 | else if (substr($data, 0, 4) === "\xFF\xFE\x00\x00") 67 | { 68 | $srctype = self::UTF32_LE; 69 | $x = 4; 70 | } 71 | else 72 | { 73 | $srctype = self::UTF32_LE; 74 | } 75 | } 76 | 77 | while ($x < $y) 78 | { 79 | // Read the next valid code point. 80 | $val = false; 81 | 82 | switch ($srctype) 83 | { 84 | case self::UTF8: 85 | { 86 | $tempchr = ord($data[$x]); 87 | if ($tempchr <= 0x7F) 88 | { 89 | $val = $tempchr; 90 | $x++; 91 | } 92 | else if ($tempchr < 0xC2) $x++; 93 | else 94 | { 95 | $left = $y - $x; 96 | if ($left < 2) $x++; 97 | else 98 | { 99 | $tempchr2 = ord($data[$x + 1]); 100 | 101 | if (($tempchr >= 0xC2 && $tempchr <= 0xDF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) 102 | { 103 | $val = (($tempchr & 0x1F) << 6) | ($tempchr2 & 0x3F); 104 | $x += 2; 105 | } 106 | else if ($left < 3) $x++; 107 | else 108 | { 109 | $tempchr3 = ord($data[$x + 2]); 110 | 111 | if ($tempchr3 < 0x80 || $tempchr3 > 0xBF) $x++; 112 | else 113 | { 114 | if (($tempchr == 0xE0 && ($tempchr2 >= 0xA0 && $tempchr2 <= 0xBF)) || ((($tempchr >= 0xE1 && $tempchr <= 0xEC) || $tempchr == 0xEE || $tempchr == 0xEF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xED && ($tempchr2 >= 0x80 && $tempchr2 <= 0x9F))) 115 | { 116 | $val = (($tempchr & 0x0F) << 12) | (($tempchr2 & 0x3F) << 6) | ($tempchr3 & 0x3F); 117 | $x += 3; 118 | } 119 | else if ($left < 4) $x++; 120 | else 121 | { 122 | $tempchr4 = ord($data[$x + 3]); 123 | 124 | if ($tempchr4 < 0x80 || $tempchr4 > 0xBF) $x++; 125 | else if (($tempchr == 0xF0 && ($tempchr2 >= 0x90 && $tempchr2 <= 0xBF)) || (($tempchr >= 0xF1 && $tempchr <= 0xF3) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xF4 && ($tempchr2 >= 0x80 && $tempchr2 <= 0x8F))) 126 | { 127 | $val = (($tempchr & 0x07) << 18) | (($tempchr2 & 0x3F) << 12) | (($tempchr3 & 0x3F) << 6) | ($tempchr4 & 0x3F); 128 | $x += 4; 129 | } 130 | else 131 | { 132 | $x++; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | break; 141 | } 142 | case self::UTF16_LE: 143 | { 144 | if ($x + 1 >= $y) $x = $y; 145 | else 146 | { 147 | $val = unpack("v", substr($data, $x, 2))[1]; 148 | $x += 2; 149 | 150 | if ($val >= 0xD800 && $val <= 0xDBFF) 151 | { 152 | if ($x + 1 >= $y) 153 | { 154 | $x = $y; 155 | $val = false; 156 | } 157 | else 158 | { 159 | $val2 = unpack("v", substr($data, $x, 2))[1]; 160 | 161 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 162 | else 163 | { 164 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 165 | $x += 2; 166 | } 167 | } 168 | } 169 | } 170 | 171 | break; 172 | } 173 | case self::UTF16_BE: 174 | { 175 | if ($x + 1 >= $y) $x = $y; 176 | else 177 | { 178 | $val = unpack("n", substr($data, $x, 2))[1]; 179 | $x += 2; 180 | 181 | if ($val >= 0xD800 && $val <= 0xDBFF) 182 | { 183 | if ($x + 1 >= $y) 184 | { 185 | $x = $y; 186 | $val = false; 187 | } 188 | else 189 | { 190 | $val2 = unpack("n", substr($data, $x, 2))[1]; 191 | 192 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 193 | else 194 | { 195 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 196 | $x += 2; 197 | } 198 | } 199 | } 200 | } 201 | 202 | break; 203 | } 204 | case self::UTF32_LE: 205 | { 206 | if ($x + 3 >= $y) $x = $y; 207 | else 208 | { 209 | $val = unpack("V", substr($data, $x, 4))[1]; 210 | $x += 4; 211 | } 212 | 213 | break; 214 | } 215 | case self::UTF32_BE: 216 | { 217 | if ($x + 3 >= $y) $x = $y; 218 | else 219 | { 220 | $val = unpack("N", substr($data, $x, 4))[1]; 221 | $x += 4; 222 | } 223 | 224 | break; 225 | } 226 | case self::UTF32_ARRAY: 227 | { 228 | $val = (int)$data[$x]; 229 | $x++; 230 | 231 | break; 232 | } 233 | default: $x = $y; break; 234 | } 235 | 236 | // Make sure it is a valid Unicode value. 237 | // 0xD800-0xDFFF are for UTF-16 surrogate pairs. Invalid characters. 238 | // 0xFDD0-0xFDEF are non-characters. 239 | // 0x*FFFE and 0x*FFFF are reserved. 240 | // The largest possible character is 0x10FFFF. 241 | // First character can't be a combining code point. 242 | if ($val !== false && !($val < 0 || ($val >= 0xD800 && $val <= 0xDFFF) || ($val >= 0xFDD0 && $val <= 0xFDEF) || ($val & 0xFFFE) == 0xFFFE || $val > 0x10FFFF || ($first && self::IsCombiningCodePoint($val)))) 243 | { 244 | if ($first) 245 | { 246 | if ($desttype === self::UTF8_BOM) 247 | { 248 | $result .= "\xEF\xBB\xBF"; 249 | 250 | $desttype = self::UTF8; 251 | } 252 | 253 | if ($desttype === self::UTF16_BOM) 254 | { 255 | $result .= "\xFF\xFE"; 256 | 257 | $desttype = self::UTF16_LE; 258 | } 259 | 260 | if ($srctype === self::UTF32_BOM) 261 | { 262 | $result .= "\xFF\xFE\x00\x00"; 263 | 264 | $desttype = self::UTF32_LE; 265 | } 266 | 267 | $first = false; 268 | } 269 | 270 | switch ($desttype) 271 | { 272 | case self::UTF8: 273 | { 274 | if ($val <= 0x7F) $result .= chr($val); 275 | else if ($val <= 0x7FF) $result .= chr(0xC0 | ($val >> 6)) . chr(0x80 | ($val & 0x3F)); 276 | else if ($val <= 0xFFFF) $result .= chr(0xE0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 277 | else if ($val <= 0x10FFFF) $result .= chr(0xF0 | ($val >> 18)) . chr(0x80 | (($val >> 12) & 0x3F)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 278 | 279 | break; 280 | } 281 | case self::UTF16_LE: 282 | { 283 | if ($val <= 0xFFFF) $result .= pack("v", $val); 284 | else 285 | { 286 | $val -= 0x10000; 287 | $result .= pack("v", ((($val >> 10) & 0x3FF) + 0xD800)); 288 | $result .= pack("v", (($val & 0x3FF) + 0xDC00)); 289 | } 290 | 291 | break; 292 | } 293 | case self::UTF16_BE: 294 | { 295 | if ($val <= 0xFFFF) $result .= pack("n", $val); 296 | else 297 | { 298 | $val -= 0x10000; 299 | $result .= pack("n", ((($val >> 10) & 0x3FF) + 0xD800)); 300 | $result .= pack("n", (($val & 0x3FF) + 0xDC00)); 301 | } 302 | 303 | break; 304 | } 305 | case self::UTF32_LE: 306 | { 307 | $result .= pack("V", $val); 308 | 309 | break; 310 | } 311 | case self::UTF32_BE: 312 | { 313 | $result .= pack("N", $val); 314 | 315 | break; 316 | } 317 | case self::UTF32_ARRAY: 318 | { 319 | $result[] = $val; 320 | 321 | break; 322 | } 323 | default: $x = $y; break; 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | 332 | protected const PUNYCODE_BASE = 36; 333 | protected const PUNYCODE_TMIN = 1; 334 | protected const PUNYCODE_TMAX = 26; 335 | protected const PUNYCODE_SKEW = 38; 336 | protected const PUNYCODE_DAMP = 700; 337 | protected const PUNYCODE_INITIAL_BIAS = 72; 338 | protected const PUNYCODE_INITIAL_N = 0x80; 339 | protected const PUNYCODE_DIGIT_MAP = "abcdefghijklmnopqrstuvwxyz0123456789"; 340 | 341 | public static function ConvertToPunycode($domain) 342 | { 343 | // Reject invalid domain name lengths. 344 | if (strlen($domain) > 255) return false; 345 | 346 | $parts = explode(".", $domain); 347 | 348 | foreach ($parts as $num => $part) 349 | { 350 | // Reject invalid label lengths. 351 | $y = strlen($part); 352 | if ($y > 63) return false; 353 | 354 | // Skip already encoded portions. 355 | if (substr($part, 0, 4) === "xn--") continue; 356 | 357 | // Convert UTF-8 to UTF-32 code points. 358 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 359 | 360 | // Handle ASCII code points. 361 | $part2 = ""; 362 | foreach ($data as $cp) 363 | { 364 | if ($cp <= 0x7F) $part2 .= strtolower(chr($cp)); 365 | } 366 | 367 | $numhandled = strlen($part2); 368 | $y = count($data); 369 | 370 | if ($numhandled >= $y) 371 | { 372 | $parts[$num] = $part2; 373 | 374 | continue; 375 | } 376 | 377 | if ($numhandled) $part2 .= "-"; 378 | 379 | $part2 = "xn--" . $part2; 380 | 381 | if (strlen($part2) > 63) return false; 382 | 383 | $bias = self::PUNYCODE_INITIAL_BIAS; 384 | $n = self::PUNYCODE_INITIAL_N; 385 | $delta = 0; 386 | $first = true; 387 | 388 | while ($numhandled < $y) 389 | { 390 | // Find the next largest unhandled code point. 391 | $cp2 = 0x01000000; 392 | foreach ($data as $cp) 393 | { 394 | if ($cp >= $n && $cp2 > $cp) $cp2 = $cp; 395 | } 396 | 397 | // Increase delta but prevent overflow. 398 | $delta += ($cp2 - $n) * ($numhandled + 1); 399 | if ($delta < 0) return false; 400 | $n = $cp2; 401 | 402 | foreach ($data as $cp) 403 | { 404 | if ($cp < $n) 405 | { 406 | $delta++; 407 | 408 | if ($delta < 0) return false; 409 | } 410 | else if ($cp === $n) 411 | { 412 | // Calculate and encode a variable length integer from the delta. 413 | $q = $delta; 414 | $x = 0; 415 | do 416 | { 417 | $x += self::PUNYCODE_BASE; 418 | 419 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 420 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 421 | else $t = $x - $bias; 422 | 423 | if ($q < $t) break; 424 | 425 | $part2 .= self::PUNYCODE_DIGIT_MAP[$t + (($q - $t) % (self::PUNYCODE_BASE - $t))]; 426 | 427 | $q = (int)(($q - $t) / (self::PUNYCODE_BASE - $t)); 428 | 429 | if (strlen($part2) > 63) return false; 430 | } while (1); 431 | 432 | $part2 .= self::PUNYCODE_DIGIT_MAP[$q]; 433 | if (strlen($part2) > 63) return false; 434 | 435 | // Adapt bias. 436 | $numhandled++; 437 | $bias = self::InternalPunycodeAdapt($delta, $numhandled, $first); 438 | $delta = 0; 439 | $first = false; 440 | } 441 | } 442 | 443 | $delta++; 444 | $n++; 445 | } 446 | 447 | $parts[$num] = $part2; 448 | } 449 | 450 | return implode(".", $parts); 451 | } 452 | 453 | public static function ConvertFromPunycode($domain) 454 | { 455 | // Reject invalid domain name lengths. 456 | if (strlen($domain) > 255) return false; 457 | 458 | $parts = explode(".", $domain); 459 | 460 | foreach ($parts as $num => $part) 461 | { 462 | // Reject invalid label lengths. 463 | $y = strlen($part); 464 | if ($y > 63) return false; 465 | 466 | // Skip unencoded portions. 467 | if (substr($part, 0, 4) !== "xn--") continue; 468 | 469 | $part = substr($part, 4); 470 | 471 | // Convert UTF-8 to UTF-32 code points. 472 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 473 | 474 | // Handle ASCII code points. 475 | $hyphen = ord("-"); 476 | for ($x = count($data); $x && $data[$x - 1] !== $hyphen; $x--); 477 | if (!$x) $data2 = array(); 478 | else 479 | { 480 | $data2 = array_splice($data, 0, $x - 1); 481 | 482 | array_shift($data); 483 | } 484 | 485 | $numhandled = count($data2); 486 | 487 | $bias = self::PUNYCODE_INITIAL_BIAS; 488 | $n = self::PUNYCODE_INITIAL_N; 489 | $delta = 0; 490 | $first = true; 491 | 492 | $pos = 0; 493 | $y = count($data); 494 | while ($pos < $y) 495 | { 496 | // Calculate and decode a delta from the variable length integer. 497 | $olddelta = $delta; 498 | $w = 1; 499 | $x = 0; 500 | do 501 | { 502 | $x += self::PUNYCODE_BASE; 503 | 504 | $cp = $data[$pos]; 505 | $pos++; 506 | 507 | if ($cp >= ord("a") && $cp <= ord("z")) $digit = $cp - ord("a"); 508 | else if ($cp >= ord("A") && $cp <= ord("Z")) $digit = $cp - ord("A"); 509 | else if ($cp >= ord("0") && $cp <= ord("9")) $digit = $cp - ord("0") + 26; 510 | else return false; 511 | 512 | $delta += $digit * $w; 513 | if ($delta < 0) return false; 514 | 515 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 516 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 517 | else $t = $x - $bias; 518 | 519 | if ($digit < $t) break; 520 | 521 | $w *= (self::PUNYCODE_BASE - $t); 522 | if ($w < 0) return false; 523 | } while (1); 524 | 525 | // Adapt bias. 526 | $numhandled++; 527 | $bias = self::InternalPunycodeAdapt($delta - $olddelta, $numhandled, $first); 528 | $first = false; 529 | 530 | // Delta was supposed to wrap around from $numhandled to 0, incrementing $n each time, so fix that now. 531 | $n += (int)($delta / $numhandled); 532 | $delta %= $numhandled; 533 | 534 | // Insert $n (the code point) at the delta position. 535 | array_splice($data2, $delta, 0, array($n)); 536 | $delta++; 537 | } 538 | 539 | $parts[$num] = self::Convert($data2, self::UTF32_ARRAY, self::UTF8); 540 | } 541 | 542 | return implode(".", $parts); 543 | } 544 | 545 | // RFC3492 adapt() function. 546 | protected static function InternalPunycodeAdapt($delta, $numpoints, $first) 547 | { 548 | $delta = ($first ? (int)($delta / self::PUNYCODE_DAMP) : $delta >> 1); 549 | $delta += (int)($delta / $numpoints); 550 | 551 | $y = self::PUNYCODE_BASE - self::PUNYCODE_TMIN; 552 | 553 | $condval = (int)(($y * self::PUNYCODE_TMAX) / 2); 554 | for ($x = 0; $delta > $condval; $x += self::PUNYCODE_BASE) $delta = (int)($delta / $y); 555 | 556 | return (int)($x + ((($y + 1) * $delta) / ($delta + self::PUNYCODE_SKEW))); 557 | } 558 | } 559 | ?> -------------------------------------------------------------------------------- /support/websocket_server.php: -------------------------------------------------------------------------------- 1 | Reset(); 18 | } 19 | 20 | public function Reset() 21 | { 22 | if (!class_exists("WebSocket", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/websocket.php"; 23 | 24 | $this->fp = false; 25 | $this->clients = array(); 26 | $this->nextclientid = 1; 27 | $this->websocketclass = "WebSocket"; 28 | $this->origins = false; 29 | 30 | $this->defaultclosemode = WebSocket::CLOSE_IMMEDIATELY; 31 | $this->defaultmaxreadframesize = 2000000; 32 | $this->defaultmaxreadmessagesize = 10000000; 33 | $this->defaultkeepalive = 30; 34 | $this->lasttimeoutcheck = time(); 35 | } 36 | 37 | public function __destruct() 38 | { 39 | $this->Stop(); 40 | } 41 | 42 | public function SetWebSocketClass($newclass) 43 | { 44 | if (class_exists($newclass)) $this->websocketclass = $newclass; 45 | } 46 | 47 | public function SetAllowedOrigins($origins) 48 | { 49 | if (is_string($origins)) $origins = array($origins); 50 | if (!is_array($origins)) $origins = false; 51 | else if (isset($origins[0])) $origins = array_flip($origins); 52 | 53 | $this->origins = $origins; 54 | } 55 | 56 | public function SetDefaultCloseMode($mode) 57 | { 58 | $this->defaultclosemode = $mode; 59 | } 60 | 61 | public function SetDefaultKeepAliveTimeout($keepalive) 62 | { 63 | $this->defaultkeepalive = (int)$keepalive; 64 | } 65 | 66 | public function SetDefaultMaxReadFrameSize($maxsize) 67 | { 68 | $this->defaultmaxreadframesize = (is_bool($maxsize) ? false : (int)$maxsize); 69 | } 70 | 71 | public function SetDefaultMaxReadMessageSize($maxsize) 72 | { 73 | $this->defaultmaxreadmessagesize = (is_bool($maxsize) ? false : (int)$maxsize); 74 | } 75 | 76 | // Starts the server on the host and port. 77 | // $host is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6. 78 | public function Start($host, $port) 79 | { 80 | $this->Stop(); 81 | 82 | $this->fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr); 83 | if ($this->fp === false) return array("success" => false, "error" => self::WSTranslate("Bind() failed. Reason: %s (%d)", $errorstr, $errornum), "errorcode" => "bind_failed"); 84 | 85 | // Enable non-blocking mode. 86 | stream_set_blocking($this->fp, 0); 87 | 88 | return array("success" => true); 89 | } 90 | 91 | public function Stop() 92 | { 93 | foreach ($this->clients as $client) 94 | { 95 | if ($client->websocket !== false) $client->websocket->Disconnect(); 96 | else fclose($client->fp); 97 | } 98 | 99 | $this->clients = array(); 100 | 101 | if ($this->fp !== false) 102 | { 103 | fclose($this->fp); 104 | 105 | $this->fp = false; 106 | } 107 | 108 | $this->nextclientid = 1; 109 | } 110 | 111 | // Dangerous but allows for stream_select() calls on multiple, separate stream handles. 112 | public function GetStream() 113 | { 114 | return $this->fp; 115 | } 116 | 117 | // Return whatever response/headers are needed here. 118 | protected function ProcessNewConnection($method, $path, $client) 119 | { 120 | $result = ""; 121 | 122 | if ($method !== "GET") $result .= "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n"; 123 | else if (!isset($client->headers["Host"]) || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false || !isset($client->headers["Sec-Websocket-Key"])) 124 | { 125 | $result .= "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"; 126 | } 127 | else if (!isset($client->headers["Sec-Websocket-Version"]) || $client->headers["Sec-Websocket-Version"] != 13) 128 | { 129 | $result .= "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nConnection: close\r\n\r\n"; 130 | } 131 | else if (!isset($client->headers["Origin"]) || ($this->origins !== false && !isset($this->origins[strtolower($client->headers["Origin"])]))) 132 | { 133 | $result .= "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n"; 134 | } 135 | 136 | return $result; 137 | } 138 | 139 | // Return whatever additional HTTP headers are needed here. 140 | protected function ProcessAcceptedConnection($method, $path, $client) 141 | { 142 | return ""; 143 | } 144 | 145 | protected function InitNewClient($fp) 146 | { 147 | $client = new stdClass(); 148 | 149 | $client->id = $this->nextclientid; 150 | $client->readdata = ""; 151 | $client->writedata = ""; 152 | $client->request = false; 153 | $client->path = ""; 154 | $client->url = ""; 155 | $client->headers = array(); 156 | $client->lastheader = ""; 157 | $client->websocket = false; 158 | $client->fp = $fp; 159 | $client->ipaddr = stream_socket_get_name($fp, true); 160 | 161 | // Intended for application storage. 162 | $client->appdata = false; 163 | 164 | $this->clients[$this->nextclientid] = $client; 165 | 166 | $this->nextclientid++; 167 | 168 | return $client; 169 | } 170 | 171 | private function ProcessInitialResponse($method, $path, $client) 172 | { 173 | // Let a derived class handle the new connection (e.g. processing Origin and Host). 174 | // Since the 'websocketclass' is instantiated AFTER this function, it is possible to switch classes on the fly. 175 | $client->writedata .= $this->ProcessNewConnection($method, $path, $client); 176 | 177 | // If an error occurs, the connection will still terminate. 178 | $client->websocket = new $this->websocketclass(); 179 | $client->websocket->SetCloseMode($this->defaultclosemode); 180 | $client->websocket->SetKeepAliveTimeout($this->defaultkeepalive); 181 | 182 | // If nothing was output, accept the connection. 183 | if ($client->writedata === "") 184 | { 185 | $client->writedata .= "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"; 186 | $client->writedata .= "Sec-WebSocket-Accept: " . base64_encode(sha1($client->headers["Sec-Websocket-Key"] . WebSocket::KEY_GUID, true)) . "\r\n"; 187 | $client->writedata .= $this->ProcessAcceptedConnection($method, $path, $client); 188 | $client->writedata .= "\r\n"; 189 | 190 | // Finish class initialization. 191 | $client->websocket->SetServerMode(); 192 | $client->websocket->SetMaxReadFrameSize($this->defaultmaxreadframesize); 193 | $client->websocket->SetMaxReadMessageSize($this->defaultmaxreadmessagesize); 194 | 195 | // Set the socket in the WebSocket class. 196 | $client->websocket->Connect("", "", array("connected_fp" => $client->fp)); 197 | } 198 | 199 | $this->UpdateClientState($client->id); 200 | } 201 | 202 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 203 | { 204 | if ($this->fp !== false) $readfps[$prefix . "ws_s"] = $this->fp; 205 | if ($timeout === false || $timeout > $this->defaultkeepalive) $timeout = $this->defaultkeepalive; 206 | 207 | foreach ($this->clients as $id => $client) 208 | { 209 | if ($client->writedata === "") $readfps[$prefix . "ws_c_" . $id] = $client->fp; 210 | 211 | if ($client->writedata !== "" || ($client->websocket !== false && $client->websocket->NeedsWrite())) $writefps[$prefix . "ws_c_" . $id] = $client->fp; 212 | 213 | if ($client->websocket !== false) 214 | { 215 | $timeout2 = $client->websocket->GetKeepAliveTimeout(); 216 | if ($timeout > $timeout2) $timeout = $timeout2; 217 | } 218 | } 219 | } 220 | 221 | // Sometimes keyed arrays don't work properly. 222 | public static function FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 223 | { 224 | // In order to correctly detect bad outputs, no '0' integer key is allowed. 225 | if (isset($readfps[0]) || isset($writefps[0]) || ($exceptfps !== NULL && isset($exceptfps[0]))) return false; 226 | 227 | $origreadfps = $readfps; 228 | $origwritefps = $writefps; 229 | $origexceptfps = $exceptfps; 230 | 231 | $result2 = @stream_select($readfps, $writefps, $exceptfps, $timeout); 232 | if ($result2 === false) return false; 233 | 234 | if (isset($readfps[0])) 235 | { 236 | $fps = array(); 237 | foreach ($origreadfps as $key => $fp) $fps[(int)$fp] = $key; 238 | 239 | foreach ($readfps as $num => $fp) 240 | { 241 | $readfps[$fps[(int)$fp]] = $fp; 242 | 243 | unset($readfps[$num]); 244 | } 245 | } 246 | 247 | if (isset($writefps[0])) 248 | { 249 | $fps = array(); 250 | foreach ($origwritefps as $key => $fp) $fps[(int)$fp] = $key; 251 | 252 | foreach ($writefps as $num => $fp) 253 | { 254 | $writefps[$fps[(int)$fp]] = $fp; 255 | 256 | unset($writefps[$num]); 257 | } 258 | } 259 | 260 | if ($exceptfps !== NULL && isset($exceptfps[0])) 261 | { 262 | $fps = array(); 263 | foreach ($origexceptfps as $key => $fp) $fps[(int)$fp] = $key; 264 | 265 | foreach ($exceptfps as $num => $fp) 266 | { 267 | $exceptfps[$fps[(int)$fp]] = $fp; 268 | 269 | unset($exceptfps[$num]); 270 | } 271 | } 272 | 273 | return true; 274 | } 275 | 276 | // Handles new connections, the initial conversation, basic packet management, and timeouts. 277 | // Can wait on more streams than just sockets and/or more sockets. Useful for waiting on other resources. 278 | // 'ws_s' and the 'ws_c_' prefix are reserved. 279 | // Returns an array of clients that may need more processing. 280 | public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL) 281 | { 282 | $this->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 283 | 284 | $result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array(), "accepted" => array(), "read" => array(), "write" => array()); 285 | if (!count($readfps) && !count($writefps)) return $result; 286 | 287 | $result2 = self::FixedStreamSelect($readfps, $writefps, $exceptfps, $timeout); 288 | if ($result2 === false) return array("success" => false, "error" => self::WSTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 289 | 290 | // Return handles that were being waited on. 291 | $result["readfps"] = $readfps; 292 | $result["writefps"] = $writefps; 293 | $result["exceptfps"] = $exceptfps; 294 | 295 | $this->ProcessWaitResult($result); 296 | 297 | return $result; 298 | } 299 | 300 | protected function ProcessWaitResult(&$result) 301 | { 302 | // Handle new connections. 303 | if (isset($result["readfps"]["ws_s"])) 304 | { 305 | while (($fp = @stream_socket_accept($this->fp, 0)) !== false) 306 | { 307 | // Enable non-blocking mode. 308 | stream_set_blocking($fp, 0); 309 | 310 | $client = $this->InitNewClient($fp); 311 | 312 | $result["accepted"][$client->id] = $client; 313 | } 314 | 315 | unset($result["readfps"]["ws_s"]); 316 | } 317 | 318 | // Handle clients in the read queue. 319 | foreach ($result["readfps"] as $cid => $fp) 320 | { 321 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue; 322 | 323 | $id = (int)substr($cid, 5); 324 | 325 | if (!isset($this->clients[$id])) continue; 326 | 327 | $client = $this->clients[$id]; 328 | 329 | $result["read"][$id] = $client; 330 | 331 | if ($client->websocket !== false) 332 | { 333 | $this->ProcessClientQueuesAndTimeoutState($result, $id, true, isset($result["writefps"][$cid])); 334 | 335 | // Remove active WebSocket clients from the write queue. 336 | unset($result["writefps"][$cid]); 337 | } 338 | else 339 | { 340 | $result2 = @fread($fp, 8192); 341 | if ($result2 === false || ($result2 === "" && feof($fp))) 342 | { 343 | @fclose($fp); 344 | 345 | unset($this->clients[$id]); 346 | } 347 | else 348 | { 349 | $client->readdata .= $result2; 350 | 351 | if (strlen($client->readdata) > 100000) 352 | { 353 | // Bad header size. Just kill the connection. 354 | @fclose($fp); 355 | 356 | unset($this->clients[$id]); 357 | } 358 | else 359 | { 360 | while (($pos = strpos($client->readdata, "\n")) !== false) 361 | { 362 | // Retrieve the next line of input. 363 | $line = rtrim(substr($client->readdata, 0, $pos)); 364 | $client->readdata = (string)substr($client->readdata, $pos + 1); 365 | 366 | if ($client->request === false) $client->request = trim($line); 367 | else if ($line !== "") 368 | { 369 | // Process the header. 370 | if ($client->lastheader != "" && (substr($line, 0, 1) == " " || substr($line, 0, 1) == "\t")) $client->headers[$client->lastheader] .= $header; 371 | else 372 | { 373 | $pos = strpos($line, ":"); 374 | if ($pos === false) $pos = strlen($line); 375 | $client->lastheader = self::HeaderNameCleanup(substr($line, 0, $pos)); 376 | $client->headers[$client->lastheader] = ltrim(substr($line, $pos + 1)); 377 | } 378 | } 379 | else 380 | { 381 | // Headers have all been received. Process the client request. 382 | $request = $client->request; 383 | $pos = strpos($request, " "); 384 | if ($pos === false) $pos = strlen($request); 385 | $method = (string)substr($request, 0, $pos); 386 | $request = trim(substr($request, $pos)); 387 | 388 | $pos = strrpos($request, " "); 389 | if ($pos === false) $pos = strlen($request); 390 | $path = (string)substr($request, 0, $pos); 391 | if ($path === "") $path = "/"; 392 | 393 | if (isset($client->headers["Host"])) $client->headers["Host"] = preg_replace('/[^a-z0-9.:\[\]_-]/', "", strtolower($client->headers["Host"])); 394 | 395 | $client->path = $path; 396 | $client->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 397 | 398 | $this->ProcessInitialResponse($method, $path, $client); 399 | 400 | break; 401 | } 402 | } 403 | } 404 | } 405 | } 406 | 407 | unset($result["readfps"][$cid]); 408 | } 409 | 410 | // Handle remaining clients in the write queue. 411 | foreach ($result["writefps"] as $cid => $fp) 412 | { 413 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue; 414 | 415 | $id = (int)substr($cid, 5); 416 | 417 | if (!isset($this->clients[$id])) continue; 418 | 419 | $client = $this->clients[$id]; 420 | 421 | $result["write"][$id] = $client; 422 | 423 | if ($client->writedata === "") $this->ProcessClientQueuesAndTimeoutState($result, $id, false, true); 424 | else 425 | { 426 | $result2 = @fwrite($fp, $client->writedata); 427 | if ($result2 === false || ($result2 === "" && feof($fp))) 428 | { 429 | @fclose($fp); 430 | 431 | unset($this->clients[$id]); 432 | } 433 | else if ($result2 === 0) $this->ProcessClientQueuesAndTimeoutState($result, $id, true, false, 1); 434 | else 435 | { 436 | $client->writedata = (string)substr($client->writedata, $result2); 437 | 438 | // Let the application know about the new client or close the connection if the WebSocket Upgrade request failed. 439 | if ($client->writedata === "") 440 | { 441 | if ($client->websocket->GetStream() !== false) $result["clients"][$id] = $client; 442 | else 443 | { 444 | @fclose($fp); 445 | 446 | unset($this->clients[$id]); 447 | } 448 | } 449 | } 450 | } 451 | 452 | unset($result["writefps"][$cid]); 453 | } 454 | 455 | // Handle client timeouts. 456 | $ts = time(); 457 | if ($this->lasttimeoutcheck <= $ts - 5) 458 | { 459 | foreach ($this->clients as $id => $client) 460 | { 461 | if (!isset($result["clients"][$id]) && $client->writedata === "" && $client->websocket !== false) 462 | { 463 | $this->ProcessClientQueuesAndTimeoutState($result, $id, false, false); 464 | } 465 | } 466 | 467 | $this->lasttimeoutcheck = $ts; 468 | } 469 | } 470 | 471 | protected function ProcessClientQueuesAndTimeoutState(&$result, $id, $read, $write, $readsize = 65536) 472 | { 473 | $client = $this->clients[$id]; 474 | 475 | $result2 = $client->websocket->ProcessQueuesAndTimeoutState($read, $write, $readsize); 476 | if ($result2["success"]) $result["clients"][$id] = $client; 477 | else 478 | { 479 | $result["removed"][$id] = array("result" => $result2, "client" => $client); 480 | 481 | $this->RemoveClient($id); 482 | } 483 | } 484 | 485 | public function GetClients() 486 | { 487 | return $this->clients; 488 | } 489 | 490 | public function NumClients() 491 | { 492 | return count($this->clients); 493 | } 494 | 495 | public function UpdateClientState($id) 496 | { 497 | } 498 | 499 | public function GetClient($id) 500 | { 501 | return (isset($this->clients[$id]) ? $this->clients[$id] : false); 502 | } 503 | 504 | public function RemoveClient($id) 505 | { 506 | if (isset($this->clients[$id])) 507 | { 508 | $client = $this->clients[$id]; 509 | 510 | // Remove the client. 511 | if ($client->websocket->GetStream() !== false) 512 | { 513 | $client->websocket->Disconnect(); 514 | $client->websocket = false; 515 | $client->fp = false; 516 | } 517 | 518 | if ($client->fp !== false) @fclose($client->fp); 519 | 520 | unset($this->clients[$id]); 521 | } 522 | } 523 | 524 | public function ProcessWebServerClientUpgrade($webserver, $client) 525 | { 526 | if (!($client instanceof WebServer_Client)) return false; 527 | 528 | if (!$client->requestcomplete || $client->mode === "handle_response") return false; 529 | if ($client->request["method"] !== "GET" || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false) return false; 530 | 531 | // Create an equivalent WebSocket server client class. 532 | $webserver->DetachClient($client->id); 533 | 534 | $method = $client->request["method"]; 535 | $path = $client->request["path"]; 536 | 537 | $client2 = $this->InitNewClient($client->fp); 538 | $client2->request = $client->request["line"]; 539 | $client2->headers = $client->headers; 540 | $client2->path = $path; 541 | $client2->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 542 | 543 | $client2->appdata = $client->appdata; 544 | 545 | $this->ProcessInitialResponse($method, $path, $client2); 546 | 547 | return $client2->id; 548 | } 549 | 550 | public static function HeaderNameCleanup($name) 551 | { 552 | return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name))))); 553 | } 554 | 555 | public static function WSTranslate() 556 | { 557 | $args = func_get_args(); 558 | if (!count($args)) return ""; 559 | 560 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 561 | } 562 | } 563 | ?> -------------------------------------------------------------------------------- /support/webroute_server.php: -------------------------------------------------------------------------------- 1 | Reset(); 15 | } 16 | 17 | public function Reset() 18 | { 19 | $this->fp = false; 20 | $this->clients = array(); 21 | $this->nextclientid = 1; 22 | $this->unlinkedclients = array(); 23 | 24 | $this->maxchunksize = 65536; 25 | $this->defaulttimeout = 60; 26 | } 27 | 28 | public function __destruct() 29 | { 30 | $this->Stop(); 31 | } 32 | 33 | public function SetMaxChunkSize($maxsize) 34 | { 35 | $this->maxchunksize = (int)$maxsize; 36 | } 37 | 38 | public function SetDefaultTimeout($defaulttimeout) 39 | { 40 | $this->defaulttimeout = (int)$defaulttimeout; 41 | } 42 | 43 | // Starts the server on the host and port. 44 | // $host is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6. 45 | public function Start($host, $port) 46 | { 47 | $this->Stop(); 48 | 49 | $this->fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr); 50 | if ($this->fp === false) return array("success" => false, "error" => self::WRTranslate("Bind() failed. Reason: %s (%d)", $errorstr, $errornum), "errorcode" => "bind_failed"); 51 | 52 | // Enable non-blocking mode. 53 | stream_set_blocking($this->fp, 0); 54 | 55 | return array("success" => true); 56 | } 57 | 58 | public function Stop() 59 | { 60 | if ($this->fp !== false) 61 | { 62 | foreach ($this->clients as $id => $client) 63 | { 64 | $this->RemoveClient($id); 65 | } 66 | 67 | fclose($this->fp); 68 | 69 | $this->clients = array(); 70 | $this->fp = false; 71 | } 72 | 73 | $this->nextclientid = 1; 74 | $this->unlinkedclients = array(); 75 | } 76 | 77 | // Dangerous but allows for stream_select() calls on multiple, separate stream handles. 78 | public function GetStream() 79 | { 80 | return $this->fp; 81 | } 82 | 83 | // Return whatever response/headers are needed here. 84 | protected function ProcessNewConnection($method, $path, $client) 85 | { 86 | $result = ""; 87 | 88 | if ($method !== "GET") $result .= "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; 89 | else if (!isset($client->headers["Host"]) || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "webroute") === false || !isset($client->headers["Webroute-Id"])) 90 | { 91 | $result .= "HTTP/1.1 400 Bad Request\r\n\r\n"; 92 | } 93 | else if (!isset($client->headers["Webroute-Version"]) || $client->headers["Webroute-Version"] != 1) 94 | { 95 | $result .= "HTTP/1.1 426 Upgrade Required\r\nWebRoute-Version: 1\r\n\r\n"; 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | // Return whatever additional HTTP headers are needed here. 102 | protected function ProcessAcceptedConnection($method, $path, $client) 103 | { 104 | return ""; 105 | } 106 | 107 | protected function InitNewClient($fp) 108 | { 109 | $client = new stdClass(); 110 | 111 | $client->id = $this->nextclientid; 112 | $client->readdata = ""; 113 | $client->writedata = ""; 114 | $client->state = "request"; 115 | $client->request = false; 116 | $client->method = ""; 117 | $client->path = ""; 118 | $client->url = ""; 119 | $client->headers = array(); 120 | $client->lastheader = ""; 121 | $client->webrouteid = false; 122 | $client->linkid = false; 123 | $client->fp = $fp; 124 | $client->lastts = microtime(true); 125 | $client->timeout = $this->defaulttimeout; 126 | $client->rawrecvsize = 0; 127 | $client->rawsendsize = 0; 128 | 129 | // Intended for application storage. 130 | $client->appdata = false; 131 | 132 | $this->clients[$this->nextclientid] = $client; 133 | 134 | $this->nextclientid++; 135 | 136 | return $client; 137 | } 138 | 139 | private function AcceptClient($client) 140 | { 141 | $client->writedata .= "HTTP/1.1 101 Switching Protocols\r\nUpgrade: webroute\r\nConnection: Upgrade\r\n"; 142 | $client->writedata .= "Sec-WebRoute-Accept: " . base64_encode(sha1($client->webrouteid . self::ID_GUID, true)) . "\r\n"; 143 | $client->writedata .= $this->ProcessAcceptedConnection($client->method, $client->path, $client); 144 | $client->writedata .= "\r\n"; 145 | 146 | $client->state = "linked"; 147 | $client->lastts = microtime(true); 148 | } 149 | 150 | private function ProcessInitialResponse($client) 151 | { 152 | // Let a derived class handle the new connection (e.g. processing Host). 153 | $client->writedata .= $this->ProcessNewConnection($client->method, $client->path, $client); 154 | 155 | // If nothing was output, accept the connection. 156 | if ($client->writedata === "") 157 | { 158 | $client->webrouteid = $client->headers["Webroute-Id"]; 159 | 160 | // Either establish a link OR register the client with unlinked clients. 161 | $key = $client->path . ":" . $client->webrouteid; 162 | if (isset($this->unlinkedclients[$key])) 163 | { 164 | $client->linkid = $this->unlinkedclients[$key]; 165 | 166 | $client2 = $this->clients[$client->linkid]; 167 | $client2->linkid = $client->id; 168 | 169 | $this->AcceptClient($client); 170 | $this->AcceptClient($client2); 171 | 172 | $client2->writedata .= $client->readdata; 173 | $client->writedata .= $client2->readdata; 174 | $client->readdata = ""; 175 | $client2->readdata = ""; 176 | 177 | // Adjust the WebRoute timeout of both clients to the minimum agreed upon timeout. 178 | $timeout = (isset($client->headers["Webroute-Timeout"]) && is_numeric($client->headers["Webroute-Timeout"]) && (int)$client->headers["Webroute-Timeout"] > $client->timeout ? (int)$client->headers["Webroute-Timeout"] : $client->timeout); 179 | $timeout2 = (isset($client2->headers["Webroute-Timeout"]) && is_numeric($client2->headers["Webroute-Timeout"]) && (int)$client2->headers["Webroute-Timeout"] > $client2->timeout ? (int)$client2->headers["Webroute-Timeout"] : $client2->timeout); 180 | $timeout = min($timeout, $timeout2); 181 | $client->timeout = $timeout; 182 | $client2->timeout = $timeout; 183 | 184 | unset($this->unlinkedclients[$key]); 185 | } 186 | else 187 | { 188 | $this->unlinkedclients[$key] = $client->id; 189 | 190 | $client->state = "waiting"; 191 | } 192 | 193 | return true; 194 | } 195 | 196 | return false; 197 | } 198 | 199 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 200 | { 201 | if ($this->fp !== false) $readfps[$prefix . "wr_s"] = $this->fp; 202 | if ($timeout === false || $timeout > $this->defaulttimeout) $timeout = $this->defaulttimeout; 203 | 204 | foreach ($this->clients as $id => $client) 205 | { 206 | if ($client->state === "request" || ($client->state === "waiting" && $client->lastts < microtime(true) - 1) || ($client->state === "linked" && strlen($this->clients[$client->linkid]->writedata) < $this->maxchunksize)) $readfps[$prefix . "wr_c_" . $id] = $client->fp; 207 | 208 | if ($client->writedata !== "") $writefps[$prefix . "wr_c_" . $id] = $client->fp; 209 | 210 | if ($client->state === "closing" && $client->writedata === "") $timeout = 0; 211 | } 212 | } 213 | 214 | // Sometimes keyed arrays don't work properly. 215 | public static function FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 216 | { 217 | // In order to correctly detect bad outputs, no '0' integer key is allowed. 218 | if (isset($readfps[0]) || isset($writefps[0]) || ($exceptfps !== NULL && isset($exceptfps[0]))) return false; 219 | 220 | $origreadfps = $readfps; 221 | $origwritefps = $writefps; 222 | $origexceptfps = $exceptfps; 223 | 224 | $result2 = @stream_select($readfps, $writefps, $exceptfps, $timeout); 225 | if ($result2 === false) return false; 226 | 227 | if (isset($readfps[0])) 228 | { 229 | $fps = array(); 230 | foreach ($origreadfps as $key => $fp) $fps[(int)$fp] = $key; 231 | 232 | foreach ($readfps as $num => $fp) 233 | { 234 | $readfps[$fps[(int)$fp]] = $fp; 235 | 236 | unset($readfps[$num]); 237 | } 238 | } 239 | 240 | if (isset($writefps[0])) 241 | { 242 | $fps = array(); 243 | foreach ($origwritefps as $key => $fp) $fps[(int)$fp] = $key; 244 | 245 | foreach ($writefps as $num => $fp) 246 | { 247 | $writefps[$fps[(int)$fp]] = $fp; 248 | 249 | unset($writefps[$num]); 250 | } 251 | } 252 | 253 | if ($exceptfps !== NULL && isset($exceptfps[0])) 254 | { 255 | $fps = array(); 256 | foreach ($origexceptfps as $key => $fp) $fps[(int)$fp] = $key; 257 | 258 | foreach ($exceptfps as $num => $fp) 259 | { 260 | $exceptfps[$fps[(int)$fp]] = $fp; 261 | 262 | unset($exceptfps[$num]); 263 | } 264 | } 265 | 266 | return true; 267 | } 268 | 269 | // Handles new connections, the initial conversation, basic packet management, and timeouts. 270 | // Can wait on more streams than just sockets and/or more sockets. Useful for waiting on other resources. 271 | // 'wr_s' and the 'wr_c_' prefix are reserved. 272 | // Returns an array of clients that may need more processing. 273 | public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL) 274 | { 275 | $this->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 276 | 277 | $result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array()); 278 | if (!count($readfps) && !count($writefps)) return $result; 279 | 280 | $result2 = self::FixedStreamSelect($readfps, $writefps, $exceptfps, $timeout); 281 | if ($result2 === false) return array("success" => false, "error" => self::WRTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 282 | 283 | // Handle new connections. 284 | if (isset($readfps["wr_s"])) 285 | { 286 | while (($fp = @stream_socket_accept($this->fp, 0)) !== false) 287 | { 288 | // Enable non-blocking mode. 289 | stream_set_blocking($fp, 0); 290 | 291 | $this->InitNewClient($fp); 292 | } 293 | 294 | unset($readfps["wr_s"]); 295 | } 296 | 297 | // Handle clients in the read queue. 298 | foreach ($readfps as $cid => $fp) 299 | { 300 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "wr_c_") continue; 301 | 302 | $id = (int)substr($cid, 5); 303 | 304 | if (!isset($this->clients[$id])) continue; 305 | 306 | $client = $this->clients[$id]; 307 | 308 | if ($client->state === "linked") 309 | { 310 | $result2 = @fread($fp, 8192); 311 | if ($result2 === false || ($result2 === "" && feof($fp))) 312 | { 313 | $this->RemoveClient($id); 314 | 315 | $result["removed"][$id] = array("result" => array("success" => false, "error" => self::WRTranslate("Client fread() failure. Most likely cause: Connection failure."), "errorcode" => "fread_failed"), "client" => $client); 316 | } 317 | else 318 | { 319 | $this->clients[$client->linkid]->writedata .= $result2; 320 | 321 | $client->lastts = microtime(true); 322 | $client->rawrecvsize += strlen($result2); 323 | 324 | $result["clients"][$id] = $client; 325 | } 326 | } 327 | else if ($client->state === "waiting") 328 | { 329 | $result2 = @fread($fp, 1); 330 | if ($result2 === false || ($result2 === "" && feof($fp))) 331 | { 332 | $this->RemoveClient($id); 333 | 334 | $result["removed"][$id] = array("result" => array("success" => false, "error" => self::WRTranslate("Client fread() failure. Most likely cause: Connection failure."), "errorcode" => "fread_failed"), "client" => $client); 335 | } 336 | else 337 | { 338 | $client->readdata .= $result2; 339 | 340 | $client->lastts = microtime(true); 341 | $client->rawrecvsize += strlen($result2); 342 | } 343 | } 344 | else if ($client->state === "request") 345 | { 346 | $result2 = @fread($fp, 8192); 347 | if ($result2 === false || ($result2 === "" && feof($fp))) $this->RemoveClient($id); 348 | else 349 | { 350 | $client->readdata .= $result2; 351 | $client->lastts = microtime(true); 352 | $client->rawrecvsize += strlen($result2); 353 | 354 | if (strlen($client->readdata) > 100000) 355 | { 356 | // Bad header size. Just kill the connection. 357 | @fclose($fp); 358 | 359 | unset($this->clients[$id]); 360 | } 361 | else 362 | { 363 | while (($pos = strpos($client->readdata, "\n")) !== false) 364 | { 365 | // Retrieve the next line of input. 366 | $line = rtrim(substr($client->readdata, 0, $pos)); 367 | $client->readdata = (string)substr($client->readdata, $pos + 1); 368 | 369 | if ($client->request === false) $client->request = trim($line); 370 | else if ($line !== "") 371 | { 372 | // Process the header. 373 | if ($client->lastheader != "" && (substr($line, 0, 1) == " " || substr($line, 0, 1) == "\t")) $client->headers[$client->lastheader] .= $header; 374 | else 375 | { 376 | $pos = strpos($line, ":"); 377 | if ($pos === false) $pos = strlen($line); 378 | $client->lastheader = self::HeaderNameCleanup(substr($line, 0, $pos)); 379 | $client->headers[$client->lastheader] = ltrim(substr($line, $pos + 1)); 380 | } 381 | } 382 | else 383 | { 384 | // Headers have all been received. Process the client request. 385 | $request = $client->request; 386 | $pos = strpos($request, " "); 387 | if ($pos === false) $pos = strlen($request); 388 | $method = (string)substr($request, 0, $pos); 389 | $request = trim(substr($request, $pos)); 390 | 391 | $pos = strrpos($request, " "); 392 | if ($pos === false) $pos = strlen($request); 393 | $path = (string)substr($request, 0, $pos); 394 | if ($path === "") $path = "/"; 395 | 396 | if (isset($client->headers["Host"])) $client->headers["Host"] = preg_replace('/[^a-z0-9.:\[\]_-]/', "", strtolower($client->headers["Host"])); 397 | 398 | $client->method = $method; 399 | $client->path = $path; 400 | $client->url = "wr://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 401 | 402 | if ($this->ProcessInitialResponse($client)) $result["clients"][$id] = $client; 403 | 404 | break; 405 | } 406 | } 407 | } 408 | } 409 | } 410 | 411 | unset($readfps[$cid]); 412 | } 413 | 414 | // Handle remaining clients in the write queue. 415 | foreach ($writefps as $cid => $fp) 416 | { 417 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "wr_c_") continue; 418 | 419 | $id = (int)substr($cid, 5); 420 | 421 | if (!isset($this->clients[$id])) continue; 422 | 423 | $client = $this->clients[$id]; 424 | 425 | if ($client->writedata !== "") 426 | { 427 | $result2 = @fwrite($fp, $client->writedata); 428 | if ($result2 === false || ($result2 === "" && feof($fp))) 429 | { 430 | $this->RemoveClient($id); 431 | 432 | $result["removed"][$id] = array("result" => array("success" => false, "error" => self::WRTranslate("Client fwrite() failure. Most likely cause: Connection failure."), "errorcode" => "fwrite_failed"), "client" => $client); 433 | } 434 | else if ($result2 === 0) 435 | { 436 | // Verify that the connection is still okay (doesn't matter if any data is read in). 437 | $result2 = @fread($fp, 1); 438 | if ($result2 === false || ($result2 === "" && feof($fp))) 439 | { 440 | $this->RemoveClient($id); 441 | 442 | $result["removed"][$id] = array("result" => array("success" => false, "error" => self::WRTranslate("Client fread() failure. Most likely cause: Connection failure."), "errorcode" => "fread_failed"), "client" => $client); 443 | } 444 | else if ($client->state === "linked") 445 | { 446 | $this->clients[$client->linkid]->writedata .= $result2; 447 | 448 | $client->lastts = microtime(true); 449 | $client->rawrecvsize += strlen($result2); 450 | 451 | $result["clients"][$id] = $client; 452 | } 453 | else 454 | { 455 | $client->readdata .= $result2; 456 | } 457 | } 458 | else 459 | { 460 | $client->writedata = (string)substr($client->writedata, $result2); 461 | $client->lastts = microtime(true); 462 | $client->rawsendsize += $result2; 463 | 464 | $result["clients"][$id] = $client; 465 | } 466 | } 467 | 468 | unset($writefps[$cid]); 469 | } 470 | 471 | // Handle client timeouts. 472 | foreach ($this->clients as $id => $client) 473 | { 474 | if (($client->state === "closing" && $client->writedata === "") || (!isset($result["clients"][$id]) && $client->lastts < microtime(true) - $client->timeout)) 475 | { 476 | if ($client->state === "waiting") 477 | { 478 | // Send a failure response. 479 | $client->writedata .= "HTTP/1.1 504 Gateway Timeout\r\nWebRoute-Version: 1\r\nConnection: close\r\n\r\n"; 480 | 481 | $client->state = "closing"; 482 | 483 | $key = $client->path . ":" . $client->webrouteid; 484 | unset($this->unlinkedclients[$key]); 485 | } 486 | else 487 | { 488 | $this->RemoveClient($id); 489 | 490 | $result["removed"][$id] = array("result" => array("success" => false, "error" => self::WRTranslate("Client timeout. Most likely cause: Connection failure."), "errorcode" => "connection_timeout"), "client" => $client); 491 | } 492 | } 493 | } 494 | 495 | // Return any extra handles that were being waited on. 496 | $result["readfps"] = $readfps; 497 | $result["writefps"] = $writefps; 498 | $result["exceptfps"] = $exceptfps; 499 | 500 | return $result; 501 | } 502 | 503 | public function GetClients() 504 | { 505 | return $this->clients; 506 | } 507 | 508 | public function GetClient($id) 509 | { 510 | return (isset($this->clients[$id]) ? $this->clients[$id] : false); 511 | } 512 | 513 | public function RemoveClient($id) 514 | { 515 | if (isset($this->clients[$id])) 516 | { 517 | $client = $this->clients[$id]; 518 | 519 | if ($client->fp !== false) @fclose($client->fp); 520 | 521 | if ($client->linkid !== false) 522 | { 523 | $this->clients[$client->linkid]->linkid = false; 524 | $this->clients[$client->linkid]->state = "closing"; 525 | } 526 | else if ($client->state === "waiting") 527 | { 528 | $key = $client->path . ":" . $client->webrouteid; 529 | 530 | unset($this->unlinkedclients[$key]); 531 | } 532 | 533 | unset($this->clients[$id]); 534 | } 535 | } 536 | 537 | public function ProcessWebServerClientUpgrade($webserver, $client, $linkexists = false) 538 | { 539 | if (!($client instanceof WebServer_Client)) return false; 540 | 541 | if (!$client->requestcomplete || $client->mode === "handle_response") return false; 542 | if ($client->request["method"] !== "GET" || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "webroute") === false) return false; 543 | 544 | // Only attempt the upgrade if a link already exists. 545 | // Useful for preventing timeouts on broken connections. 546 | if ($linkexists && isset($client->headers["Webroute-Id"]) && !isset($this->unlinkedclients[$client->request["path"] . ":" . $client->headers["Webroute-Id"]])) return false; 547 | 548 | // Create an equivalent WebRoute server client class. 549 | $webserver->DetachClient($client->id); 550 | 551 | $method = $client->request["method"]; 552 | $path = $client->request["path"]; 553 | 554 | $client2 = $this->InitNewClient($client->fp); 555 | $client2->request = $client->request["line"]; 556 | $client2->headers = $client->headers; 557 | $client2->method = $method; 558 | $client2->path = $path; 559 | $client2->url = "wr://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 560 | 561 | $client2->appdata = $client->appdata; 562 | 563 | $this->ProcessInitialResponse($client2); 564 | 565 | return $client2->id; 566 | } 567 | 568 | public static function HeaderNameCleanup($name) 569 | { 570 | return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name))))); 571 | } 572 | 573 | public static function WRTranslate() 574 | { 575 | $args = func_get_args(); 576 | if (!count($args)) return ""; 577 | 578 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 579 | } 580 | } 581 | ?> --------------------------------------------------------------------------------