├── tests ├── test_webroute_client.php └── test_webroute_server.php ├── support ├── crc32_stream.php ├── webroute.php ├── deflate_stream.php ├── random.php ├── utf_utils.php ├── webroute_server.php └── web_browser.php ├── docs ├── webroute.md └── webroute_server.md └── README.md /tests/test_webroute_client.php: -------------------------------------------------------------------------------- 1 | Connect("wr://127.0.0.1:47735/test?accesskey=123456789101112", $id); 14 | if (!$result["success"]) 15 | { 16 | var_dump($result); 17 | exit(); 18 | } 19 | 20 | // At this point, the socket is simply a TCP/IP passthrough. 21 | $fp = $result["fp"]; 22 | 23 | fwrite($fp, "CUSTOM PROTOCOL HERE\n"); 24 | echo fgets($fp); 25 | sleep(1); 26 | 27 | fwrite($fp, "QUIT\n"); 28 | echo fgets($fp); 29 | sleep(1); 30 | 31 | fclose($fp); 32 | ?> -------------------------------------------------------------------------------- /tests/test_webroute_server.php: -------------------------------------------------------------------------------- 1 | Start("127.0.0.1", "47735"); 14 | if (!$result["success"]) 15 | { 16 | var_dump($result); 17 | exit(); 18 | } 19 | 20 | echo "Ready.\n"; 21 | 22 | do 23 | { 24 | $result = $wrserver->Wait(); 25 | 26 | // Do something with active clients. 27 | foreach ($result["clients"] as $id => $client) 28 | { 29 | if ($client->appdata === false) 30 | { 31 | echo "Client ID " . $id . " connected.\n"; 32 | 33 | // Example of checking for an access key. 34 | $url = HTTP::ExtractURL($client->url); 35 | if (!isset($url["queryvars"]["accesskey"]) || $url["queryvars"]["accesskey"][0] !== "123456789101112") 36 | { 37 | $wrserver->RemoveClient($id); 38 | 39 | continue; 40 | } 41 | 42 | echo "Valid access key used.\n"; 43 | 44 | $client->appdata = array(); 45 | } 46 | } 47 | 48 | // Do something with removed clients. 49 | foreach ($result["removed"] as $id => $result2) 50 | { 51 | if ($result2["client"]->appdata !== false) 52 | { 53 | echo "Client ID " . $id . " disconnected.\n"; 54 | 55 | // echo "Client ID " . $id . " disconnected. Reason:\n"; 56 | // var_dump($result2["result"]); 57 | // echo "\n"; 58 | } 59 | } 60 | } while (1); 61 | ?> -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /docs/webroute.md: -------------------------------------------------------------------------------- 1 | WebRoute Class: 'support/webroute.php' 2 | ======================================= 3 | 4 | This class provides client-side routines to initiate a connection with another WebRoute enabled client via a WebRoute server (RFC ????). 5 | 6 | Example usage: 7 | 8 | ```php 9 | Connect("wr://localhost:47735/test?accesskey=123456789101112", $id); 20 | if (!$result["success"]) 21 | { 22 | var_dump($result); 23 | exit(); 24 | } 25 | 26 | // At this point, the socket is simply a TCP/IP passthrough. 27 | $fp = $result["fp"]; 28 | 29 | fwrite($fp, "CUSTOM PROTOCOL HERE\n"); 30 | echo fgets($fp); 31 | sleep(1); 32 | 33 | fwrite($fp, "QUIT\n"); 34 | echo fgets($fp); 35 | sleep(1); 36 | 37 | fclose($fp); 38 | ?> 39 | ``` 40 | 41 | WebRoute::ProcessState($state) 42 | ------------------------------ 43 | 44 | Access: public 45 | 46 | Parameters: 47 | 48 | * $state - A valid WebRoute state object. 49 | 50 | Returns: A standard array of information. 51 | 52 | This internal-ish function runs the core state engine behind the scenes against the input state. This is the primary workhorse of the WebRoute class. 53 | 54 | WebRoute::Connect($url, $id = false, $options = array(), $web = false) 55 | ---------------------------------------------------------------------- 56 | 57 | Access: public 58 | 59 | Parameters: 60 | 61 | * $url - A string containing a WebRoute URL (starts with wr:// or wrs://). 62 | * $id - A boolean of false to generate an ID via the CubicleSoft CSPRNG class or a string containing an ID generated with a CSPRNG (Default is false). 63 | * $options - An array of valid WebBrowser class options (Default is array()). 64 | * $web - A valid WebBrowser class instance (Default is false, which means one will be created). 65 | 66 | Returns: An array containing the results of the call. 67 | 68 | This function initiates a connection to a WebRoute server via the WebBrowser class. If you set up your own WebBrowser class (e.g. to handle cookies), pass it in as the $web parameter to use your class instance for the connection. 69 | 70 | WebRoute::ConnectAsync($helper, $key, $callback, $url, $id = false, $options = array(), $web = false) 71 | ----------------------------------------------------------------------------------------------------- 72 | 73 | Access: public 74 | 75 | Parameters: 76 | 77 | * $helper - A MultiAsyncHelper instance. 78 | * $key - A string containing a key to uniquely identify this WebBrowser instance. 79 | * $callback - An optional callback function to receive regular status updates on the request (specify NULL if not needed). The callback function must accept three parameters - callback($key, $url, $result). 80 | * $url - A string containing a WebRoute URL (starts with wr:// or wrs://). 81 | * $id - A boolean of false to generate an ID via the CubicleSoft CSPRNG class or a string containing an ID generated with a CSPRNG (Default is false). 82 | * $options - An array of valid WebBrowser class options (Default is array()). 83 | * $web - A valid WebBrowser class instance (Default is false, which means one will be created). 84 | 85 | Returns: A standard array of information. 86 | 87 | This function queues the request with the MultiAsyncHandler instance ($helper) for later async/non-blocking processing of the request. Note that this function always succeeds since request failure can't be detected until after processing begins. 88 | 89 | See MultiAsyncHelper for example usage. 90 | 91 | WebRoute::ConnectAsync__Handler($mode, &$data, $key, &$info) 92 | ------------------------------------------------------------ 93 | 94 | Access: _internal_ public 95 | 96 | Parameters: 97 | 98 | * $mode - A string representing the mode/state to process. 99 | * $data - Mixed content the depends entirely on the $mode. 100 | * $key - A string representing the key associated with an object. 101 | * $info - The information associated with the key. 102 | 103 | Returns: Nothing. 104 | 105 | This internal static callback function is the internal handler for MultiAsyncHandler for processing WebRoute class instances. 106 | 107 | WebRoute::WRTranslate($format, ...) 108 | ----------------------------------- 109 | 110 | Access: _internal_ static 111 | 112 | Parameters: 113 | 114 | * $format - A string containing valid sprintf() format specifiers. 115 | 116 | Returns: A string containing a translation. 117 | 118 | This internal static function takes input strings and translates them from English to some other language if CS_TRANSLATE_FUNC is defined to be a valid PHP function name. 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebRoute 2 | ======== 3 | 4 | This is the official reference implementation of the WebRoute Internet protocol. 5 | 6 | The WebRoute protocol links two TCP/IP endpoints together via shared unique IDs and then transitions linked connections into a simple TCP/IP passthrough. A WebRoute server is intended to be deployed as a reverse proxy behind a standard web server (e.g. Apache or Nginx). The method by which clients initiate a connection is not dissimilar to WebSocket Upgrade but without all of the framing baggage that WebSocket brings to the table. 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 | Features 11 | -------- 12 | 13 | * Connects two clients together via unique ID regardless of how each client's firewall is setup. 14 | * Uses ubiquitous web server technology to initiate a TCP/IP passthrough. 15 | * The protocol is designed to be easily integrated into existing software products that already support WebSocket (e.g. web browsers) by following a similar pattern. 16 | * The reference implementation has a liberal open source license. MIT or LGPL, your choice. 17 | * Designed for relatively painless integration into your environment. 18 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 19 | 20 | Test Suite 21 | ---------- 22 | 23 | Included with this repository are a test server and client, written in PHP. To run the test suite, follow these simple steps using separate terminal/console/command line windows: 24 | 25 | * Start the server by running `php tests/test_webroute_server.php`. This starts a basic WebRoute server that is designed for protocol demonstration purposes only. 26 | * Start the first client by running `php tests/test_webroute_client.php`. This connects to the WebRoute server with a static ID and enters a waiting state. 27 | * Start the second client by running `php tests/test_webroute_client.php`. This connects to the WebRoute server with the same static ID as the first client. 28 | 29 | When the second client connects, the WebRoute server sees that there is a waiting client with the same ID as the second client (the first client). At this point, the server links the two clients together and data is sent, received, and echoed out to the display using a custom protocol (i.e. no longer HTTP). 30 | 31 | The WebRoute Protocol 32 | --------------------- 33 | 34 | The WebRoute protocol itself is licensed under Creative Commons Zero (CC0). There's not much to it anyway. 35 | 36 | The WebRoute protocol follows an Upgrade approach that is very similar to WebSocket. Two clients connect into a WebRoute server and make a request that looks like: 37 | 38 | ``` 39 | GET /test?accesskey=123456789101112 HTTP/1.1 40 | Host: 127.0.0.1:47735 41 | Connection: keep-alive, Upgrade 42 | WebRoute-Version: 1 43 | WebRoute-ID: 6fa1a1490ee0fe617b36530054e984786897e022 44 | WebRoute-Timeout: 60 45 | Upgrade: webroute 46 | 47 | ``` 48 | 49 | Upon accepting the upgrade request, usually only after a "link" is established between both clients (e.g. updating pointers in internal structures), the server responds to each client with: 50 | 51 | ``` 52 | HTTP/1.1 101 Switching Protocols 53 | Upgrade: webroute 54 | Connection: Upgrade 55 | Sec-WebRoute-Accept: FgtwdI1ph0NTOVQts+nD5u0AcXs= 56 | 57 | ``` 58 | 59 | The value of `Sec-WebRoute-Accept` is calculated as `base64-encode(sha1(concatenate(client-WebRoute-ID, "BE7204BD-47E6-49EE-9B0D-016E370644B2"))))`. 60 | 61 | Upon upgrading to WebRoute, any data sent to the server is simply passed through to the other client that was associated by the same `WebRoute-ID`. 62 | 63 | Some additional details about the various headers sent by each client: 64 | 65 | * WebRoute-Version - An integer representing the version of the protocol. Currently must be `1`. 66 | * WebRoute-ID - A unique, hard to guess ID, preferably generated with a CSPRNG. Required. Additional security measures should be taken (e.g. options in the URL or a custom HTTP header). Like WebSocket, relying solely on HTTP cookies is not recommended as they can used in cross-site request forgery (CSRF/XSRF) attacks. 67 | * WebRoute-Timeout - An optional integer representing the number of seconds to wait before timing out the connection AFTER the WebRoute request has been accepted by the server. This is a suggestion to the WebRoute server as to how long to keep the connection alive when no data has been sent or received from either side. A WebRoute server should not accept timeout values less than its own minimum timeout but should otherwise honor the smallest timeout value sent by both clients. For example, if the server timeout is 60 seconds and two clients send 45 and 65, the chosen timeout for both clients should be 60. Another example is if the server timeout is 60 seconds and two clients send 80 and 120, the chosen timeout for both clients should be 80. 68 | 69 | It is recommended that 'wr://' and 'wrs://' (SSL) be used to recognize WebRoute URIs. In addition, it is possible to chain URIs together to build a chain of TCP/IP passthroughs via WebRoute. This implementation uses the space ' ' character to separate URIs and will return the first URI that does not begin with 'wr://' or 'wrs://'. 70 | 71 | Note that neither the protocol nor the recommended protocol schemes have been registered with the IETF. If someone wants to contribute toward a Draft/Standard, you are certainly welcome to commit valuable time toward such a goal. I looked at the lengthy, involved process and decided that it wasn't worth my time. 72 | 73 | Security Notes 74 | -------------- 75 | 76 | The WebRoute protocol is only as secure as the unique identifiers in use. A PRNG or a weak CSPRNG could result in insecure, guessable IDs being generated and therefore linking two clients together that should not be linked. Additional security measures should be taken when using upgrade protocols like WebRoute. 77 | 78 | The WebRoute protocol can be utilized to completely bypass most common firewall setups in ways that are very unusual and difficult to detect. See [Remoted API Server](https://github.com/cubiclesoft/remoted-api-server) for such an implementation. Like most of the sage advice regarding network-enabled software that can be used on the Internet: Caution is advised when deploying WebRoute to production systems. Know what you are doing when it comes to using upgrade protocols like WebRoute or it'll come back to haunt you. 79 | -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /docs/webroute_server.md: -------------------------------------------------------------------------------- 1 | WebRouteServer Class: 'support/webroute_server.php' 2 | ==================================================== 3 | 4 | This class is a working WebRoute server implementation in PHP. It won't win any performance awards. However, this class avoids the need to set up a formal web server or compile anything and then figure out how to proxy server Upgrade requests via that server. 5 | 6 | Pairs nicely with the WebServer class for handing off Upgrade requests to the WebRouteServer class via the WebRouteServer::ProcessWebServerClientUpgrade() function. 7 | 8 | For example basic usage, see: 9 | 10 | https://github.com/cubiclesoft/webroute/blob/master/tests/test_webroute_server.php 11 | 12 | WebRouteServer::Reset() 13 | ----------------------- 14 | 15 | Access: public 16 | 17 | Parameters: None. 18 | 19 | Returns: Nothing. 20 | 21 | This function resets a class instance to the default state. Note that WebRouteServer::Stop() should be called first as this simply resets all internal class variables. 22 | 23 | WebRouteServer::SetMaxChunkSize($maxsize) 24 | ----------------------------------------- 25 | 26 | Access: public 27 | 28 | Parameters: 29 | 30 | * $maxsize - An integer representing the maximum chunk size, in bytes, to queue per client. 31 | 32 | Returns: Nothing. 33 | 34 | This function sets the maximum chunk size for data passthrough to manage server RAM usage. The default setting is 65,536 bytes. 35 | 36 | WebRouteServer::SetDefaultTimeout($defaulttimeout) 37 | -------------------------------------------------- 38 | 39 | Access: public 40 | 41 | Parameters: 42 | 43 | * $defaulttimeout - An integer representing the default socket timeout, in seconds, per client. 44 | 45 | Returns: Nothing. 46 | 47 | This function sets the default period of inactivity before closing the socket. The default setting is 60 seconds. 48 | 49 | WebRouteServer::Start($host, $port) 50 | ----------------------------------- 51 | 52 | Access: public 53 | 54 | Parameters: 55 | 56 | * $host - A string containing the host IP to bind to. 57 | * $port - A port number to bind to. On some systems, ports under 1024 are restricted to root/admin level access only. 58 | 59 | Returns: An array containing the results of the call. 60 | 61 | This function starts a WebRoute server on the specified host and port. The socket is also set to asynchronous (non-blocking) mode. Some useful values for the host: 62 | 63 | * `127.0.0.1` to bind to the localhost IPv4 interface. 64 | * `0.0.0.0` to bind to all IPv4 interfaces. 65 | * `[::1]` to bind to the localhost IPv6 interface. 66 | * `[::0]` to bind to all IPv6 interfaces. 67 | 68 | To select a new port number for a server, use the following link: 69 | 70 | https://www.random.org/integers/?num=1&min=5001&max=49151&col=5&base=10&format=html&rnd=new 71 | 72 | If it shows port 8080, just reload to get a different port number. 73 | 74 | WebRouteServer::Stop() 75 | ---------------------- 76 | 77 | Access: public 78 | 79 | Parameters: None. 80 | 81 | Returns: Nothing. 82 | 83 | This function stops a WebRoute server after disconnecting all clients and resets some internal variables in case the class instance is reused. 84 | 85 | WebRouteServer::GetStream() 86 | --------------------------- 87 | 88 | Access: public 89 | 90 | Parameters: None. 91 | 92 | Returns: The underlying socket stream (PHP resource) or a boolean of false if the server is not started. 93 | 94 | This function is considered "dangerous" as it allows direct access to the underlying stream. However, as long as it is only used with functions like PHP stream_select() and Wait() is used to do actual management, it should be safe enough. This function is intended to be used where there are multiple handles being waited on (e.g. handling multiple connections to multiple WebRoute servers). 95 | 96 | WebRouteServer::UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 97 | ---------------------------------------------------------------------------------- 98 | 99 | Access: public 100 | 101 | Parameters: 102 | 103 | * $prefix - A unique prefix to identify the various streams (server and client handles). 104 | * $timeout - An integer reference containing the maximum number of seconds or a boolean of false. 105 | * $readfps - An array reference to add streams wanting data to arrive. 106 | * $writefps - An array reference to add streams wanting to send data. 107 | 108 | Returns: Nothing. 109 | 110 | This function updates the timeout and read/write arrays with prefixed names so that a single stream_select() call can manage all sockets. 111 | 112 | WebRouteServer::FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 113 | ------------------------------------------------------------------------------- 114 | 115 | Access: public static 116 | 117 | Parameters: Same as stream_select() minus the microsecond parameter. 118 | 119 | Returns: A boolean of true on success, false on failure. 120 | 121 | This function allows key-value pairs to work properly for the usual read, write, and except arrays. PHP's stream_select() function is buggy and sometimes will return correct keys and other times not. This function is called by Wait(). Directly calling this function is useful if multiple servers are running at a time (e.g. one public SSL server, one localhost non-SSL server). 122 | 123 | WebRouteServer::Wait($timeout = false) 124 | -------------------------------------- 125 | 126 | Access: public 127 | 128 | Parameters: 129 | 130 | * $timeout - A boolean of false or an integer containing the number of seconds to wait for an event to trigger such as a write operation to complete (Default is false). 131 | 132 | Returns: An array containing the results of the call. 133 | 134 | This function is the core of the WebRouteServer class and should be called frequently (e.g. a while loop). It handles new connections, the initial conversation, linking clients together, passing data between clients, and timeouts. The extra optional arrays to the call allow the function to wait on more than just sockets, which is useful when waiting on other asynchronous resources. 135 | 136 | This function returns an array of clients that were responsive during the call. It will also return clients that are no longer connected so that the application can have a chance to clean up resources associated with the client. 137 | 138 | WebRouteServer::GetClients() 139 | ---------------------------- 140 | 141 | Access: public 142 | 143 | Parameters: None. 144 | 145 | Returns: An array of all of the active clients. 146 | 147 | This function makes it easy to retrieve the entire list of clients currently connected to the server. Note that this may include clients that are in the process of connecting and upgrading to the WebRoute protocol. 148 | 149 | WebRouteServer::GetClient($id) 150 | ------------------------------ 151 | 152 | Access: public 153 | 154 | Parameters: 155 | 156 | * $id - An integer containing the ID of the client to retrieve. 157 | 158 | Returns: An array containing client information associated with the ID if it exists, a boolean of false otherwise. 159 | 160 | This function retrieves a single client array by its ID. 161 | 162 | WebRouteServer::RemoveClient($id) 163 | --------------------------------- 164 | 165 | Access: public 166 | 167 | Parameters: 168 | 169 | * $id - An integer containing the ID of the client to retrieve. 170 | 171 | Returns: Nothing. 172 | 173 | This function terminates a specified client by ID. This is the correct way to disconnect a client. Do not use fclose() directly on the socket handle. 174 | 175 | WebRouteServer::ProcessWebServerClientUpgrade($webserver, $client, $linkexists = false) 176 | --------------------------------------------------------------------------------------- 177 | 178 | Access: public 179 | 180 | Parameters: 181 | 182 | * $webserver - An instance of the WebServer class. 183 | * $client - An instance of WebServer_Client directly associated with the WebServer class. 184 | * $linkexists - A boolean indicating that the upgrade should only happen if a waiting client exists (Default is false). 185 | 186 | Returns: An integer representing the new WebRouteServer client ID on success, false otherwise. 187 | 188 | This function determines if the client is attempting to Upgrade to WebRoute. If so, it detaches the client from the WebServer instance and associates a new client with the WebRouteServer instance. Can optionally only perform the upgrade if another client exists that can be linked to. Note that the WebRouteServer instance does not require WebRouteServer::Start() to have been called. 189 | 190 | WebRouteServer::ProcessNewConnection($method, $path, $client) 191 | ------------------------------------------------------------- 192 | 193 | Access: protected 194 | 195 | Parameters: 196 | 197 | * $method - A string containing the HTTP method (supposed to be "GET"). 198 | * $path - A string containing the path portion of the request. 199 | * $client - An object containing introductory information about the new client (parsed headers, etc). 200 | 201 | Returns: A string containing the HTTP response, if any, an empty string otherwise. 202 | 203 | This function handles basic requirements of the WebRoute protocol and will reject obviously bad connections with the appropriate HTTP response string. However, the function can be overridden in a derived class. 204 | 205 | WebRouteServer::ProcessAcceptedConnection($method, $path, $client) 206 | ------------------------------------------------------------------ 207 | 208 | Access: protected 209 | 210 | Parameters: 211 | 212 | * $method - A string containing the HTTP method (supposed to be "GET"). 213 | * $path - A string containing the path portion of the request. 214 | * $client - An object containing nearly complete information about the new client (parsed headers, etc). 215 | 216 | Returns: A string containing additional HTTP headers to add to the response, if any, otherwise an empty string. 217 | 218 | This function is called if the connection is being accepted. That is, ProcessNewConnection() returned an empty string. The default function does nothing but it can be overridden in a derived class to handle things such as custom protocols and extensions. 219 | 220 | WebRouteServer::InitNewClient($fp) 221 | ---------------------------------- 222 | 223 | Access: protected 224 | 225 | Parameters: 226 | 227 | * $fp - A stream resource or a boolean of false. 228 | 229 | Returns: The new stdClass instance. 230 | 231 | This function creates a new client object. Since there isn't anything terribly complex about the object, stdClass is used instead of something formal. 232 | 233 | WebRouteServer::AcceptClient($client) 234 | ------------------------------------- 235 | 236 | Access: private 237 | 238 | Parameters: 239 | 240 | * $client - An object containing nearly complete information about the new client (parsed headers, etc). 241 | 242 | Returns: Nothing. 243 | 244 | This function queues up the Upgrade response to send to each client upon linking two clients together. 245 | 246 | WebRouteServer::ProcessInitialResponse($method, $path, $client) 247 | --------------------------------------------------------------- 248 | 249 | Access: private 250 | 251 | Parameters: 252 | 253 | * $method - A string containing the HTTP method (supposed to be "GET"). 254 | * $path - A string containing the path portion of the request. 255 | * $client - An object containing nearly complete information about the new client (parsed headers, etc). 256 | 257 | Returns: Nothing. 258 | 259 | This function performs a standard initial response to the client as to whether or not their request to Upgrade to the WebRoute protocol was successful and the client was linked to a matching client. 260 | 261 | WebRouteServer::HeaderNameCleanup($name) 262 | ---------------------------------------- 263 | 264 | Access: _internal_ static 265 | 266 | Parameters: 267 | 268 | * $name - A string containing a HTTP header name. 269 | 270 | Returns: A string containing a purified HTTP header name. 271 | 272 | This internal static function cleans up a HTTP header name. 273 | 274 | WebRouteServer::WRTranslate($format, ...) 275 | ----------------------------------------- 276 | 277 | Access: _internal_ static 278 | 279 | Parameters: 280 | 281 | * $format - A string containing valid sprintf() format specifiers. 282 | 283 | Returns: A string containing a translation. 284 | 285 | This internal static function takes input strings and translates them from English to some other language if CS_TRANSLATE_FUNC is defined to be a valid PHP function name. 286 | -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /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 | ?> -------------------------------------------------------------------------------- /support/web_browser.php: -------------------------------------------------------------------------------- 1 | ResetState(); 15 | $this->SetState($prevstate); 16 | $this->html = false; 17 | } 18 | 19 | public function ResetState() 20 | { 21 | $this->data = array( 22 | "allowedprotocols" => array("http" => true, "https" => true), 23 | "allowedredirprotocols" => array("http" => true, "https" => true), 24 | "hostauths" => array(), 25 | "cookies" => array(), 26 | "referer" => "", 27 | "autoreferer" => true, 28 | "useragent" => "firefox", 29 | "followlocation" => true, 30 | "maxfollow" => 20, 31 | "extractforms" => false, 32 | "httpopts" => array(), 33 | ); 34 | } 35 | 36 | public function SetState($options = array()) 37 | { 38 | $this->data = array_merge($this->data, $options); 39 | } 40 | 41 | public function GetState() 42 | { 43 | return $this->data; 44 | } 45 | 46 | public function ProcessState(&$state) 47 | { 48 | while ($state["state"] !== "done") 49 | { 50 | switch ($state["state"]) 51 | { 52 | case "initialize": 53 | { 54 | if (!isset($this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) || !$this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) 55 | { 56 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed in '%s'.", $state["urlinfo"]["scheme"], $state["url"]), "errorcode" => "allowed_protocols"); 57 | } 58 | 59 | $filename = HTTP::ExtractFilename($state["urlinfo"]["path"]); 60 | $pos = strrpos($filename, "."); 61 | $fileext = ($pos !== false ? strtolower(substr($filename, $pos + 1)) : ""); 62 | 63 | // Set up some standard headers. 64 | $headers = array(); 65 | $profile = strtolower($state["profile"]); 66 | $tempprofile = explode("-", $profile); 67 | if (count($tempprofile) == 2) 68 | { 69 | $profile = $tempprofile[0]; 70 | $fileext = $tempprofile[1]; 71 | } 72 | if (substr($profile, 0, 2) == "ie" || ($profile == "auto" && substr($this->data["useragent"], 0, 2) == "ie")) 73 | { 74 | if ($fileext == "css") $headers["Accept"] = "text/css"; 75 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5"; 76 | else if ($fileext == "js") $headers["Accept"] = "application/javascript, */*;q=0.8"; 77 | else if ($this->data["referer"] != "" || $fileext == "" || $fileext == "html" || $fileext == "xhtml" || $fileext == "xml") $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 78 | else $headers["Accept"] = "*/*"; 79 | 80 | $headers["Accept-Language"] = "en-US"; 81 | $headers["User-Agent"] = HTTP::GetUserAgent(substr($profile, 0, 2) == "ie" ? $profile : $this->data["useragent"]); 82 | } 83 | else if ($profile == "firefox" || ($profile == "auto" && $this->data["useragent"] == "firefox")) 84 | { 85 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 86 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png,image/*;q=0.8,*/*;q=0.5"; 87 | else if ($fileext == "js") $headers["Accept"] = "*/*"; 88 | else $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 89 | 90 | $headers["Accept-Language"] = "en-us,en;q=0.5"; 91 | $headers["Cache-Control"] = "max-age=0"; 92 | $headers["User-Agent"] = HTTP::GetUserAgent("firefox"); 93 | } 94 | else if ($profile == "opera" || ($profile == "auto" && $this->data["useragent"] == "opera")) 95 | { 96 | // Opera has the right idea: Just send the same thing regardless of the request type. 97 | $headers["Accept"] = "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"; 98 | $headers["Accept-Language"] = "en-US,en;q=0.9"; 99 | $headers["Cache-Control"] = "no-cache"; 100 | $headers["User-Agent"] = HTTP::GetUserAgent("opera"); 101 | } 102 | else if ($profile == "safari" || $profile == "edge" || $profile == "chrome" || ($profile == "auto" && ($this->data["useragent"] == "safari" || $this->data["useragent"] == "edge" || $this->data["useragent"] == "chrome"))) 103 | { 104 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 105 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg" || $fileext == "js") $headers["Accept"] = "*/*"; 106 | else $headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; 107 | 108 | $headers["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"; 109 | $headers["Accept-Language"] = "en-US,en;q=0.8"; 110 | $headers["User-Agent"] = HTTP::GetUserAgent($profile == "safari" || $profile == "chrome" ? $profile : $this->data["useragent"]); 111 | } 112 | 113 | if ($this->data["referer"] != "") $headers["Referer"] = $this->data["referer"]; 114 | 115 | // Generate the final headers array. 116 | $headers = array_merge($headers, $state["httpopts"]["headers"], $state["tempoptions"]["headers"]); 117 | 118 | // Calculate the host and reverse host and remove port information. 119 | $host = (isset($headers["Host"]) ? $headers["Host"] : $state["urlinfo"]["host"]); 120 | $pos = strpos($host, "]"); 121 | if (substr($host, 0, 1) == "[" && $pos !== false) 122 | { 123 | $host = substr($host, 0, $pos + 1); 124 | } 125 | else 126 | { 127 | $pos = strpos($host, ":"); 128 | if ($pos !== false) $host = substr($host, 0, $pos); 129 | } 130 | $dothost = $host; 131 | $dothost = strtolower($dothost); 132 | if (substr($dothost, 0, 1) != ".") $dothost = "." . $dothost; 133 | $state["dothost"] = $dothost; 134 | 135 | // Append Authorization header. 136 | if (isset($headers["Authorization"])) $this->data["hostauths"][$host] = $headers["Authorization"]; 137 | else if (isset($this->data["hostauths"][$host])) $headers["Authorization"] = $this->data["hostauths"][$host]; 138 | 139 | // Append cookies and delete old, invalid cookies. 140 | $secure = ($state["urlinfo"]["scheme"] == "https"); 141 | $cookiepath = $state["urlinfo"]["path"]; 142 | if ($cookiepath == "") $cookiepath = "/"; 143 | $pos = strrpos($cookiepath, "/"); 144 | if ($pos !== false) $cookiepath = substr($cookiepath, 0, $pos + 1); 145 | $state["cookiepath"] = $cookiepath; 146 | $cookies = array(); 147 | foreach ($this->data["cookies"] as $domain => $paths) 148 | { 149 | if (strlen($dothost) >= strlen($domain) && substr($dothost, -strlen($domain)) === $domain) 150 | { 151 | foreach ($paths as $path => $cookies2) 152 | { 153 | if (substr($cookiepath, 0, strlen($path)) == $path) 154 | { 155 | foreach ($cookies2 as $num => $info) 156 | { 157 | if (isset($info["expires_ts"]) && $this->GetExpiresTimestamp($info["expires_ts"]) < time()) unset($this->data["cookies"][$domain][$path][$num]); 158 | else if ($secure || !isset($info["secure"])) $cookies[$info["name"]] = $info["value"]; 159 | } 160 | 161 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 162 | } 163 | } 164 | 165 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 166 | } 167 | } 168 | 169 | $cookies2 = array(); 170 | foreach ($cookies as $name => $value) $cookies2[] = rawurlencode($name) . "=" . rawurlencode($value); 171 | $headers["Cookie"] = implode("; ", $cookies2); 172 | if ($headers["Cookie"] == "") unset($headers["Cookie"]); 173 | 174 | // Generate the final options array. 175 | $state["options"] = array_merge($state["httpopts"], $state["tempoptions"]); 176 | $state["options"]["headers"] = $headers; 177 | if ($state["timeout"] !== false) $state["options"]["timeout"] = HTTP::GetTimeLeft($state["startts"], $state["timeout"]); 178 | 179 | // Let a callback handle any additional state changes. 180 | if (isset($state["options"]["pre_retrievewebpage_callback"]) && is_callable($state["options"]["pre_retrievewebpage_callback"]) && !call_user_func_array($state["options"]["pre_retrievewebpage_callback"], array(&$state))) 181 | { 182 | return array("success" => false, "error" => self::WBTranslate("Pre-RetrieveWebpage callback returned with a failure condition for '%s'.", $state["url"]), "errorcode" => "pre_retrievewebpage_callback"); 183 | } 184 | 185 | // Process the request. 186 | $result = HTTP::RetrieveWebpage($state["url"], $state["options"]); 187 | $result["url"] = $state["url"]; 188 | unset($state["options"]["files"]); 189 | unset($state["options"]["body"]); 190 | unset($state["tempoptions"]["headers"]["Content-Type"]); 191 | $result["options"] = $state["options"]; 192 | $result["firstreqts"] = $state["startts"]; 193 | $result["numredirects"] = $state["numredirects"]; 194 | $result["redirectts"] = $state["redirectts"]; 195 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 196 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 197 | if (!$result["success"]) return array("success" => false, "error" => self::WBTranslate("Unable to retrieve content. %s", $result["error"]), "info" => $result, "state" => $state, "errorcode" => "retrievewebpage"); 198 | 199 | if (isset($state["options"]["async"]) && $state["options"]["async"]) 200 | { 201 | $state["async"] = true; 202 | $state["httpstate"] = $result["state"]; 203 | 204 | $state["state"] = "process_async"; 205 | } 206 | else 207 | { 208 | $state["result"] = $result; 209 | 210 | $state["state"] = "post_retrieval"; 211 | } 212 | 213 | break; 214 | } 215 | case "process_async": 216 | { 217 | // Run a cycle of the HTTP state processor. 218 | $result = HTTP::ProcessState($state["httpstate"]); 219 | if (!$result["success"]) return $result; 220 | 221 | $result["url"] = $state["url"]; 222 | $result["options"] = $state["options"]; 223 | unset($result["options"]["files"]); 224 | unset($result["options"]["body"]); 225 | $result["firstreqts"] = $state["startts"]; 226 | $result["numredirects"] = $state["numredirects"]; 227 | $result["redirectts"] = $state["redirectts"]; 228 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 229 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 230 | 231 | $state["httpstate"] = false; 232 | $state["result"] = $result; 233 | 234 | $state["state"] = "post_retrieval"; 235 | 236 | break; 237 | } 238 | case "post_retrieval": 239 | { 240 | // Set up structures for another round. 241 | if ($this->data["autoreferer"]) $this->data["referer"] = $state["url"]; 242 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"]) 243 | { 244 | $state["redirectts"] = microtime(true); 245 | 246 | unset($state["tempoptions"]["method"]); 247 | unset($state["tempoptions"]["write_body_callback"]); 248 | unset($state["tempoptions"]["body"]); 249 | unset($state["tempoptions"]["postvars"]); 250 | unset($state["tempoptions"]["files"]); 251 | 252 | $state["tempoptions"]["headers"]["Referer"] = $state["url"]; 253 | $state["url"] = $state["result"]["headers"]["Location"][0]; 254 | 255 | // Generate an absolute URL. 256 | if ($this->data["referer"] != "") $state["url"] = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $state["url"]); 257 | 258 | $urlinfo2 = HTTP::ExtractURL($state["url"]); 259 | 260 | if (!isset($this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) || !$this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) 261 | { 262 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed. Server attempted to redirect to '%s'.", $urlinfo2["scheme"], $state["url"]), "info" => $state["result"], "errorcode" => "allowed_redir_protocols"); 263 | } 264 | 265 | if ($urlinfo2["host"] != $state["urlinfo"]["host"]) 266 | { 267 | unset($state["tempoptions"]["headers"]["Host"]); 268 | unset($state["httpopts"]["headers"]["Host"]); 269 | 270 | unset($state["httpopts"]["headers"]["Authorization"]); 271 | unset($state["tempoptions"]["headers"]["Authorization"]); 272 | } 273 | 274 | $state["urlinfo"] = $urlinfo2; 275 | $state["numredirects"]++; 276 | } 277 | 278 | // Handle any 'Set-Cookie' headers. 279 | if (isset($state["result"]["headers"]["Set-Cookie"])) 280 | { 281 | foreach ($state["result"]["headers"]["Set-Cookie"] as $cookie) 282 | { 283 | $items = explode(";", $cookie); 284 | $item = trim(array_shift($items)); 285 | if ($item != "") 286 | { 287 | $cookie2 = array(); 288 | $pos = strpos($item, "="); 289 | if ($pos === false) 290 | { 291 | $cookie2["name"] = urldecode($item); 292 | $cookie2["value"] = ""; 293 | } 294 | else 295 | { 296 | $cookie2["name"] = urldecode(substr($item, 0, $pos)); 297 | $cookie2["value"] = urldecode(substr($item, $pos + 1)); 298 | } 299 | 300 | $cookie = array(); 301 | foreach ($items as $item) 302 | { 303 | $item = trim($item); 304 | if ($item != "") 305 | { 306 | $pos = strpos($item, "="); 307 | if ($pos === false) $cookie[strtolower(trim(urldecode($item)))] = ""; 308 | else $cookie[strtolower(trim(urldecode(substr($item, 0, $pos))))] = urldecode(substr($item, $pos + 1)); 309 | } 310 | } 311 | $cookie = array_merge($cookie, $cookie2); 312 | 313 | if (isset($cookie["expires"])) 314 | { 315 | $ts = HTTP::GetDateTimestamp($cookie["expires"]); 316 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", ($ts === false ? time() - 24 * 60 * 60 : $ts)); 317 | } 318 | else if (isset($cookie["max-age"])) 319 | { 320 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", time() + (int)$cookie["max-age"]); 321 | } 322 | else 323 | { 324 | unset($cookie["expires_ts"]); 325 | } 326 | 327 | if (!isset($cookie["domain"])) $cookie["domain"] = $state["dothost"]; 328 | if (!isset($cookie["path"])) $cookie["path"] = $state["cookiepath"]; 329 | 330 | $this->SetCookie($cookie); 331 | } 332 | } 333 | } 334 | 335 | if ($state["numfollow"] > 0) $state["numfollow"]--; 336 | 337 | // If this is a redirect, handle it by starting over. 338 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"] && $state["numfollow"]) 339 | { 340 | $state["result"] = false; 341 | 342 | $state["state"] = "initialize"; 343 | } 344 | else 345 | { 346 | $state["result"]["numredirects"] = $state["numredirects"]; 347 | $state["result"]["redirectts"] = $state["redirectts"]; 348 | 349 | // Extract the forms from the page in a parsed format. 350 | // Call WebBrowser::GenerateFormRequest() to prepare an actual request for Process(). 351 | if ($this->data["extractforms"]) $state["result"]["forms"] = $this->ExtractForms($state["result"]["url"], $state["result"]["body"], (isset($state["tempoptions"]["extractforms_hint"]) ? $state["tempoptions"]["extractforms_hint"] : false)); 352 | 353 | $state["state"] = "done"; 354 | } 355 | 356 | break; 357 | } 358 | } 359 | } 360 | 361 | return $state["result"]; 362 | } 363 | 364 | public function Process($url, $tempoptions = array()) 365 | { 366 | $startts = microtime(true); 367 | $redirectts = $startts; 368 | 369 | // Handle older function call: Process($url, $profile, $tempoptions) 370 | if (is_string($tempoptions)) 371 | { 372 | $args = func_get_args(); 373 | if (count($args) < 3) $tempoptions = array(); 374 | else $tempoptions = $args[2]; 375 | 376 | $tempoptions["profile"] = $args[1]; 377 | } 378 | 379 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 380 | 381 | if (isset($tempoptions["timeout"])) $timeout = $tempoptions["timeout"]; 382 | else if (isset($this->data["httpopts"]["timeout"])) $timeout = $this->data["httpopts"]["timeout"]; 383 | else $timeout = false; 384 | 385 | // Deal with possible application hanging issues. 386 | if (isset($tempoptions["streamtimeout"])) $streamtimeout = $tempoptions["streamtimeout"]; 387 | else if (isset($this->data["httpopts"]["streamtimeout"])) $streamtimeout = $this->data["httpopts"]["streamtimeout"]; 388 | else $streamtimeout = 300; 389 | $tempoptions["streamtimeout"] = $streamtimeout; 390 | 391 | if (!isset($this->data["httpopts"]["headers"])) $this->data["httpopts"]["headers"] = array(); 392 | $this->data["httpopts"]["headers"] = HTTP::NormalizeHeaders($this->data["httpopts"]["headers"]); 393 | unset($this->data["httpopts"]["method"]); 394 | unset($this->data["httpopts"]["write_body_callback"]); 395 | unset($this->data["httpopts"]["body"]); 396 | unset($this->data["httpopts"]["postvars"]); 397 | unset($this->data["httpopts"]["files"]); 398 | 399 | $httpopts = $this->data["httpopts"]; 400 | $numfollow = $this->data["maxfollow"]; 401 | $numredirects = 0; 402 | $totalrawsendsize = 0; 403 | 404 | if (!isset($tempoptions["headers"])) $tempoptions["headers"] = array(); 405 | $tempoptions["headers"] = HTTP::NormalizeHeaders($tempoptions["headers"]); 406 | if (isset($tempoptions["headers"]["Referer"])) $this->data["referer"] = $tempoptions["headers"]["Referer"]; 407 | 408 | // If a referrer is specified, use it to generate an absolute URL. 409 | if ($this->data["referer"] != "") $url = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $url); 410 | 411 | $urlinfo = HTTP::ExtractURL($url); 412 | 413 | // Initialize the process state array. 414 | $state = array( 415 | "async" => false, 416 | "startts" => $startts, 417 | "redirectts" => $redirectts, 418 | "timeout" => $timeout, 419 | "tempoptions" => $tempoptions, 420 | "httpopts" => $httpopts, 421 | "numfollow" => $numfollow, 422 | "numredirects" => $numredirects, 423 | "totalrawsendsize" => $totalrawsendsize, 424 | "profile" => $profile, 425 | "url" => $url, 426 | "urlinfo" => $urlinfo, 427 | 428 | "state" => "initialize", 429 | "httpstate" => false, 430 | "result" => false, 431 | ); 432 | 433 | // Run at least one state cycle to properly initialize the state array. 434 | $result = $this->ProcessState($state); 435 | 436 | // Return the state for async calls. Caller must call ProcessState(). 437 | if ($state["async"]) return array("success" => true, "state" => $state); 438 | 439 | return $result; 440 | } 441 | 442 | // Implements the correct MultiAsyncHelper responses for WebBrowser instances. 443 | public function ProcessAsync__Handler($mode, &$data, $key, &$info) 444 | { 445 | switch ($mode) 446 | { 447 | case "init": 448 | { 449 | if ($info["init"]) $data = $info["keep"]; 450 | else 451 | { 452 | $info["result"] = $this->Process($info["url"], $info["tempoptions"]); 453 | if (!$info["result"]["success"]) 454 | { 455 | $info["keep"] = false; 456 | 457 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 458 | } 459 | else 460 | { 461 | $info["state"] = $info["result"]["state"]; 462 | 463 | // Move to the live queue. 464 | $data = true; 465 | } 466 | } 467 | 468 | break; 469 | } 470 | case "update": 471 | case "read": 472 | case "write": 473 | { 474 | if ($info["keep"]) 475 | { 476 | $info["result"] = $this->ProcessState($info["state"]); 477 | if ($info["result"]["success"] || $info["result"]["errorcode"] !== "no_data") $info["keep"] = false; 478 | 479 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 480 | 481 | if ($mode === "update") $data = $info["keep"]; 482 | } 483 | 484 | break; 485 | } 486 | case "readfps": 487 | { 488 | if ($info["state"]["httpstate"] !== false && HTTP::WantRead($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 489 | 490 | break; 491 | } 492 | case "writefps": 493 | { 494 | if ($info["state"]["httpstate"] !== false && HTTP::WantWrite($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 495 | 496 | break; 497 | } 498 | case "cleanup": 499 | { 500 | // When true, caller is removing. Otherwise, detaching from the queue. 501 | if ($data === true) 502 | { 503 | if (isset($info["state"])) 504 | { 505 | if ($info["state"]["httpstate"] !== false) HTTP::ForceClose($info["state"]["httpstate"]); 506 | 507 | unset($info["state"]); 508 | } 509 | 510 | $info["keep"] = false; 511 | } 512 | 513 | break; 514 | } 515 | } 516 | } 517 | 518 | public function ProcessAsync($helper, $key, $callback, $url, $tempoptions = array()) 519 | { 520 | $tempoptions["async"] = true; 521 | 522 | // Handle older function call: ProcessAsync($helper, $key, $callback, $url, $profile, $tempoptions) 523 | if (is_string($tempoptions)) 524 | { 525 | $args = func_get_args(); 526 | if (count($args) < 6) $tempoptions = array(); 527 | else $tempoptions = $args[5]; 528 | 529 | $tempoptions["profile"] = $args[4]; 530 | } 531 | 532 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 533 | 534 | $info = array( 535 | "init" => false, 536 | "keep" => true, 537 | "callback" => $callback, 538 | "url" => $url, 539 | "tempoptions" => $tempoptions, 540 | "result" => false 541 | ); 542 | 543 | $helper->Set($key, $info, array($this, "ProcessAsync__Handler")); 544 | 545 | return array("success" => true); 546 | } 547 | 548 | public function ExtractForms($baseurl, $data, $hint = false) 549 | { 550 | $result = array(); 551 | 552 | $lasthint = ""; 553 | $hintmap = array(); 554 | if ($this->html === false) 555 | { 556 | if (!class_exists("simple_html_dom", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/simple_html_dom.php"; 557 | 558 | $this->html = new simple_html_dom(); 559 | } 560 | $this->html->load($data); 561 | $rows = $this->html->find("label[for]"); 562 | foreach ($rows as $row) 563 | { 564 | $hintmap[trim($row->for)] = trim($row->plaintext); 565 | } 566 | $html5rows = $this->html->find("input[form],textarea[form],select[form],button[form],datalist[id]" . ($hint !== false ? "," . $hint : "")); 567 | $rows = $this->html->find("form"); 568 | foreach ($rows as $row) 569 | { 570 | $info = array(); 571 | if (isset($row->id)) $info["id"] = trim($row->id); 572 | if (isset($row->name)) $info["name"] = (string)$row->name; 573 | $info["action"] = (isset($row->action) ? HTTP::ConvertRelativeToAbsoluteURL($baseurl, (string)$row->action) : $baseurl); 574 | $info["method"] = (isset($row->method) && strtolower(trim($row->method)) == "post" ? "post" : "get"); 575 | if ($info["method"] == "post") $info["enctype"] = (isset($row->enctype) ? strtolower($row->enctype) : "application/x-www-form-urlencoded"); 576 | if (isset($row->{"accept-charset"})) $info["accept-charset"] = (string)$row->{"accept-charset"}; 577 | 578 | $fields = array(); 579 | $rows2 = $row->find("input,textarea,select,button" . ($hint !== false ? "," . $hint : "")); 580 | foreach ($rows2 as $row2) 581 | { 582 | if (!isset($row2->form)) 583 | { 584 | if (isset($row2->id) && $row2->id != "" && isset($hintmap[trim($row2->id)])) $lasthint = $hintmap[trim($row2->id)]; 585 | 586 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 587 | } 588 | } 589 | 590 | // Handle HTML5. 591 | if (isset($info["id"]) && $info["id"] != "") 592 | { 593 | foreach ($html5rows as $row2) 594 | { 595 | if (strpos(" " . $info["id"] . " ", " " . $row2->form . " ") !== false) 596 | { 597 | if (isset($hintmap[$info["id"]])) $lasthint = $hintmap[$info["id"]]; 598 | 599 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 600 | } 601 | } 602 | } 603 | 604 | $form = new WebBrowserForm(); 605 | $form->info = $info; 606 | $form->fields = $fields; 607 | $result[] = $form; 608 | } 609 | 610 | return $result; 611 | } 612 | 613 | private function ExtractFieldFromDOM(&$fields, $row, &$lasthint) 614 | { 615 | switch ($row->tag) 616 | { 617 | case "input": 618 | { 619 | if (!isset($row->name) && ($row->type === "submit" || $row->type === "image")) $row->name = ""; 620 | 621 | if (isset($row->name) && is_string($row->name)) 622 | { 623 | $field = array( 624 | "id" => (isset($row->id) ? (string)$row->id : false), 625 | "type" => "input." . (isset($row->type) ? strtolower($row->type) : "text"), 626 | "name" => $row->name, 627 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 628 | ); 629 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") 630 | { 631 | $field["checked"] = (isset($row->checked)); 632 | 633 | if ($field["value"] === "") $field["value"] = "on"; 634 | } 635 | 636 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 637 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image") $field["hint"] = $field["type"] . "|" . $field["value"]; 638 | else if ($lasthint !== "") $field["hint"] = $lasthint; 639 | 640 | $fields[] = $field; 641 | 642 | $lasthint = ""; 643 | } 644 | 645 | break; 646 | } 647 | case "textarea": 648 | { 649 | if (isset($row->name) && is_string($row->name)) 650 | { 651 | $field = array( 652 | "id" => (isset($row->id) ? (string)$row->id : false), 653 | "type" => "textarea", 654 | "name" => $row->name, 655 | "value" => html_entity_decode($row->innertext, ENT_COMPAT, "UTF-8") 656 | ); 657 | 658 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 659 | else if ($lasthint !== "") $field["hint"] = $lasthint; 660 | 661 | $fields[] = $field; 662 | 663 | $lasthint = ""; 664 | } 665 | 666 | break; 667 | } 668 | case "select": 669 | { 670 | if (isset($row->name) && is_string($row->name)) 671 | { 672 | if (isset($row->multiple)) 673 | { 674 | // Change the type into multiple checkboxes. 675 | $rows = $row->find("option"); 676 | foreach ($rows as $row2) 677 | { 678 | $field = array( 679 | "id" => (isset($row->id) ? (string)$row->id : false), 680 | "type" => "input.checkbox", 681 | "name" => $row->name, 682 | "value" => (isset($row2->value) ? html_entity_decode($row2->value, ENT_COMPAT, "UTF-8") : ""), 683 | "display" => (string)$row2->innertext 684 | ); 685 | if ($lasthint !== "") $field["hint"] = $lasthint; 686 | 687 | $fields[] = $field; 688 | } 689 | } 690 | else 691 | { 692 | $val = false; 693 | $options = array(); 694 | $rows = $row->find("option"); 695 | foreach ($rows as $row2) 696 | { 697 | $options[$row2->value] = (string)$row2->innertext; 698 | 699 | if ($val === false && isset($row2->selected)) $val = html_entity_decode($row2->value, ENT_COMPAT, "UTF-8"); 700 | } 701 | if ($val === false && count($options)) 702 | { 703 | $val = array_keys($options); 704 | $val = $val[0]; 705 | } 706 | if ($val === false) $val = ""; 707 | 708 | $field = array( 709 | "id" => (isset($row->id) ? (string)$row->id : false), 710 | "type" => "select", 711 | "name" => $row->name, 712 | "value" => $val, 713 | "options" => $options 714 | ); 715 | if ($lasthint !== "") $field["hint"] = $lasthint; 716 | 717 | $fields[] = $field; 718 | } 719 | 720 | $lasthint = ""; 721 | } 722 | 723 | break; 724 | } 725 | case "button": 726 | { 727 | if (isset($row->name) && is_string($row->name)) 728 | { 729 | $field = array( 730 | "id" => (isset($row->id) ? (string)$row->id : false), 731 | "type" => "button." . (isset($row->type) ? strtolower($row->type) : "submit"), 732 | "name" => $row->name, 733 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 734 | ); 735 | $field["hint"] = (trim($row->plaintext) !== "" ? trim($row->plaintext) : "button|" . $field["value"]); 736 | 737 | $fields[] = $field; 738 | 739 | $lasthint = ""; 740 | } 741 | 742 | break; 743 | } 744 | case "datalist": 745 | { 746 | // Do nothing since browsers don't actually enforce this tag's values. 747 | 748 | break; 749 | } 750 | default: 751 | { 752 | // Hint for the next element. 753 | $lasthint = (string)$row->plaintext; 754 | 755 | break; 756 | } 757 | } 758 | } 759 | 760 | public static function InteractiveFormFill($forms, $showselected = false) 761 | { 762 | if (!is_array($forms)) $forms = array($forms); 763 | 764 | if (!count($forms)) return false; 765 | 766 | if (count($forms) == 1) $form = reset($forms); 767 | else 768 | { 769 | echo self::WBTranslate("There are multiple forms available to fill out:\n"); 770 | foreach ($forms as $num => $form) 771 | { 772 | echo self::WBTranslate("\t%d:\n", $num + 1); 773 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t\t%s: %s\n", $key, $val); 774 | echo self::WBTranslate("\t\tfields: %d\n", count($form->GetVisibleFields(false))); 775 | echo self::WBTranslate("\t\tbuttons: %d\n", count($form->GetVisibleFields(true)) - count($form->GetVisibleFields(false))); 776 | echo "\n"; 777 | } 778 | 779 | do 780 | { 781 | echo self::WBTranslate("Select: "); 782 | 783 | $num = (int)trim(fgets(STDIN)) - 1; 784 | } while (!isset($forms[$num])); 785 | 786 | $form = $forms[$num]; 787 | } 788 | 789 | if ($showselected) 790 | { 791 | echo self::WBTranslate("Selected form:\n"); 792 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t%s: %s\n", $key, $val); 793 | echo "\n"; 794 | } 795 | 796 | if (count($form->GetVisibleFields(false))) 797 | { 798 | echo self::WBTranslate("Select form fields by field number to edit a field. When ready to submit the form, leave 'Field number' empty.\n\n"); 799 | 800 | do 801 | { 802 | echo self::WBTranslate("Editable form fields:\n"); 803 | foreach ($form->fields as $num => $field) 804 | { 805 | if ($field["type"] == "input.hidden" || $field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button.") continue; 806 | 807 | echo self::WBTranslate("\t%d: %s - %s\n", $num + 1, $field["name"], (is_array($field["value"]) ? json_encode($field["value"], JSON_PRETTY_PRINT) : $field["value"]) . (($field["type"] == "input.radio" || $field["type"] == "input.checkbox") ? ($field["checked"] ? self::WBTranslate(" [Y]") : self::WBTranslate(" [N]")) : "") . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 808 | } 809 | echo "\n"; 810 | 811 | do 812 | { 813 | echo self::WBTranslate("Field number: "); 814 | 815 | $num = trim(fgets(STDIN)); 816 | if ($num === "") break; 817 | 818 | $num = (int)$num - 1; 819 | } while (!isset($form->fields[$num]) || $form->fields[$num]["type"] == "input.hidden" || $form->fields[$num]["type"] == "input.submit" || $form->fields[$num]["type"] == "input.image" || $form->fields[$num]["type"] == "input.button" || substr($form->fields[$num]["type"], 0, 7) == "button."); 820 | 821 | if ($num === "") 822 | { 823 | echo "\n"; 824 | 825 | break; 826 | } 827 | 828 | $field = $form->fields[$num]; 829 | $prefix = (isset($field["hint"]) && $field["hint"] !== "" ? $field["hint"] . " | " : "") . $field["name"]; 830 | 831 | if ($field["type"] == "select") 832 | { 833 | echo self::WBTranslate("[%s] Options:\n", $prefix); 834 | foreach ($field["options"] as $key => $val) 835 | { 836 | echo self::WBTranslate("\t%s: %s\n"); 837 | } 838 | 839 | do 840 | { 841 | echo self::WBTranslate("[%s] Select: ", $prefix); 842 | 843 | $select = rtrim(fgets(STDIN)); 844 | } while (!isset($field["options"][$select])); 845 | 846 | $form->fields[$num]["value"] = $select; 847 | } 848 | else if ($field["type"] == "input.radio") 849 | { 850 | $form->SetFormValue($field["name"], $field["value"], true, "input.radio"); 851 | } 852 | else if ($field["type"] == "input.checkbox") 853 | { 854 | $form->fields[$num]["checked"] = !$field["checked"]; 855 | } 856 | else if ($field["type"] == "input.file") 857 | { 858 | do 859 | { 860 | echo self::WBTranslate("[%s] Filename: ", $prefix); 861 | 862 | $filename = rtrim(fgets(STDIN)); 863 | } while ($filename !== "" && !file_exists($filename)); 864 | 865 | if ($filename === "") $form->fields[$num]["value"] = ""; 866 | else 867 | { 868 | $form->fields[$num]["value"] = array( 869 | "filename" => $filename, 870 | "type" => "application/octet-stream", 871 | "datafile" => $filename 872 | ); 873 | } 874 | } 875 | else 876 | { 877 | echo self::WBTranslate("[%s] New value: ", $prefix); 878 | 879 | $form->fields[$num]["value"] = rtrim(fgets(STDIN)); 880 | } 881 | 882 | echo "\n"; 883 | 884 | } while (1); 885 | } 886 | 887 | $submitoptions = array(array("name" => self::WBTranslate("Default action"), "value" => self::WBTranslate("Might not work"), "hint" => "Default action")); 888 | foreach ($form->fields as $num => $field) 889 | { 890 | if ($field["type"] != "input.submit" && $field["type"] != "input.image" && $field["type"] != "input.button" && $field["type"] != "button.submit") continue; 891 | 892 | $submitoptions[] = $field; 893 | } 894 | 895 | if (count($submitoptions) <= 2) $num = count($submitoptions) - 1; 896 | else 897 | { 898 | echo self::WBTranslate("Available submit buttons:\n"); 899 | foreach ($submitoptions as $num => $field) 900 | { 901 | echo self::WBTranslate("\t%d: %s - %s\n", $num, $field["name"], $field["value"] . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 902 | } 903 | echo "\n"; 904 | 905 | do 906 | { 907 | echo self::WBTranslate("Select: "); 908 | 909 | $num = (int)fgets(STDIN); 910 | } while (!isset($submitoptions[$num])); 911 | 912 | echo "\n"; 913 | } 914 | 915 | $result = $form->GenerateFormRequest(($num ? $submitoptions[$num]["name"] : false), ($num ? $submitoptions[$num]["value"] : false)); 916 | 917 | return $result; 918 | } 919 | 920 | public function GetCookies() 921 | { 922 | return $this->data["cookies"]; 923 | } 924 | 925 | public function SetCookie($cookie) 926 | { 927 | if (!isset($cookie["domain"]) || !isset($cookie["path"]) || !isset($cookie["name"]) || !isset($cookie["value"])) return array("success" => false, "error" => self::WBTranslate("SetCookie() requires 'domain', 'path', 'name', and 'value' to be options."), "errorcode" => "missing_information"); 928 | 929 | $cookie["domain"] = strtolower($cookie["domain"]); 930 | if (substr($cookie["domain"], 0, 1) != ".") $cookie["domain"] = "." . $cookie["domain"]; 931 | 932 | $cookie["path"] = str_replace("\\", "/", $cookie["path"]); 933 | if (substr($cookie["path"], -1) != "/") $cookie["path"] = "/"; 934 | 935 | if (!isset($this->data["cookies"][$cookie["domain"]])) $this->data["cookies"][$cookie["domain"]] = array(); 936 | if (!isset($this->data["cookies"][$cookie["domain"]][$cookie["path"]])) $this->data["cookies"][$cookie["domain"]][$cookie["path"]] = array(); 937 | $this->data["cookies"][$cookie["domain"]][$cookie["path"]][$cookie["name"]] = $cookie; 938 | 939 | return array("success" => true); 940 | } 941 | 942 | // Simulates closing a web browser. 943 | public function DeleteSessionCookies() 944 | { 945 | foreach ($this->data["cookies"] as $domain => $paths) 946 | { 947 | foreach ($paths as $path => $cookies) 948 | { 949 | foreach ($cookies as $num => $info) 950 | { 951 | if (!isset($info["expires_ts"])) unset($this->data["cookies"][$domain][$path][$num]); 952 | } 953 | 954 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 955 | } 956 | 957 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 958 | } 959 | } 960 | 961 | public function DeleteCookies($domainpattern, $pathpattern, $namepattern) 962 | { 963 | foreach ($this->data["cookies"] as $domain => $paths) 964 | { 965 | if ($domainpattern == "" || substr($domain, -strlen($domainpattern)) == $domainpattern) 966 | { 967 | foreach ($paths as $path => $cookies) 968 | { 969 | if ($pathpattern == "" || substr($path, 0, strlen($pathpattern)) == $pathpattern) 970 | { 971 | foreach ($cookies as $num => $info) 972 | { 973 | if ($namepattern == "" || strpos($info["name"], $namepattern) !== false) unset($this->data["cookies"][$domain][$path][$num]); 974 | } 975 | 976 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 977 | } 978 | } 979 | 980 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 981 | } 982 | } 983 | } 984 | 985 | private function GetExpiresTimestamp($ts) 986 | { 987 | $year = (int)substr($ts, 0, 4); 988 | $month = (int)substr($ts, 5, 2); 989 | $day = (int)substr($ts, 8, 2); 990 | $hour = (int)substr($ts, 11, 2); 991 | $min = (int)substr($ts, 14, 2); 992 | $sec = (int)substr($ts, 17, 2); 993 | 994 | return gmmktime($hour, $min, $sec, $month, $day, $year); 995 | } 996 | 997 | public static function WBTranslate() 998 | { 999 | $args = func_get_args(); 1000 | if (!count($args)) return ""; 1001 | 1002 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 1003 | } 1004 | } 1005 | 1006 | class WebBrowserForm 1007 | { 1008 | public $info, $fields; 1009 | 1010 | public function __construct() 1011 | { 1012 | $this->info = array(); 1013 | $this->fields = array(); 1014 | } 1015 | 1016 | public function FindFormFields($name = false, $value = false, $type = false) 1017 | { 1018 | $fields = array(); 1019 | foreach ($this->fields as $num => $field) 1020 | { 1021 | if (($type === false || $field["type"] === $type) && ($name === false || $field["name"] === $name) && ($value === false || $field["value"] === $value)) 1022 | { 1023 | $fields[] = $field; 1024 | } 1025 | } 1026 | 1027 | return $fields; 1028 | } 1029 | 1030 | public function GetHintMap() 1031 | { 1032 | $result = array(); 1033 | foreach ($this->fields as $num => $field) 1034 | { 1035 | if (isset($field["hint"])) $result[$field["hint"]] = $field["name"]; 1036 | } 1037 | 1038 | return $result; 1039 | } 1040 | 1041 | public function GetVisibleFields($submit) 1042 | { 1043 | $result = array(); 1044 | foreach ($this->fields as $num => $field) 1045 | { 1046 | if ($field["type"] == "input.hidden" || (!$submit && ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button."))) continue; 1047 | 1048 | $result[$num] = $field; 1049 | } 1050 | 1051 | return $result; 1052 | } 1053 | 1054 | public function GetFormValue($name, $checkval = false, $type = false) 1055 | { 1056 | $val = false; 1057 | foreach ($this->fields as $field) 1058 | { 1059 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1060 | { 1061 | if (is_string($checkval)) 1062 | { 1063 | if ($checkval === $field["value"]) 1064 | { 1065 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") $val = $field["checked"]; 1066 | else $val = $field["value"]; 1067 | } 1068 | } 1069 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1070 | { 1071 | $val = $field["value"]; 1072 | } 1073 | } 1074 | } 1075 | 1076 | return $val; 1077 | } 1078 | 1079 | public function SetFormValue($name, $value, $checked = false, $type = false, $create = false) 1080 | { 1081 | $result = false; 1082 | foreach ($this->fields as $num => $field) 1083 | { 1084 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1085 | { 1086 | if ($field["type"] == "input.radio") 1087 | { 1088 | $this->fields[$num]["checked"] = ($field["value"] === $value ? $checked : false); 1089 | $result = true; 1090 | } 1091 | else if ($field["type"] == "input.checkbox") 1092 | { 1093 | if ($field["value"] === $value) $this->fields[$num]["checked"] = $checked; 1094 | $result = true; 1095 | } 1096 | else if ($field["type"] != "select" || !isset($field["options"]) || isset($field["options"][$value])) 1097 | { 1098 | $this->fields[$num]["value"] = $value; 1099 | $result = true; 1100 | } 1101 | } 1102 | } 1103 | 1104 | // Add the field if it doesn't exist. 1105 | if (!$result && $create) 1106 | { 1107 | $this->fields[] = array( 1108 | "id" => false, 1109 | "type" => ($type !== false ? $type : "input.text"), 1110 | "name" => $name, 1111 | "value" => $value, 1112 | "checked" => $checked 1113 | ); 1114 | } 1115 | 1116 | return $result; 1117 | } 1118 | 1119 | public function GenerateFormRequest($submitname = false, $submitvalue = false) 1120 | { 1121 | $method = $this->info["method"]; 1122 | $fields = array(); 1123 | $files = array(); 1124 | foreach ($this->fields as $field) 1125 | { 1126 | if ($field["type"] == "input.file") 1127 | { 1128 | if (is_array($field["value"])) 1129 | { 1130 | $field["value"]["name"] = $field["name"]; 1131 | $files[] = $field["value"]; 1132 | $method = "post"; 1133 | } 1134 | } 1135 | else if ($field["type"] == "input.reset" || $field["type"] == "button.reset") 1136 | { 1137 | } 1138 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "button.submit") 1139 | { 1140 | if (($submitname === false || $field["name"] === $submitname) && ($submitvalue === false || $field["value"] === $submitvalue)) 1141 | { 1142 | if ($submitname !== "") 1143 | { 1144 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1145 | $fields[$field["name"]][] = $field["value"]; 1146 | } 1147 | 1148 | if ($field["type"] == "input.image") 1149 | { 1150 | if (!isset($fields["x"])) $fields["x"] = array(); 1151 | $fields["x"][] = "1"; 1152 | 1153 | if (!isset($fields["y"])) $fields["y"] = array(); 1154 | $fields["y"][] = "1"; 1155 | } 1156 | } 1157 | } 1158 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1159 | { 1160 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1161 | $fields[$field["name"]][] = $field["value"]; 1162 | } 1163 | } 1164 | 1165 | if ($method == "get") 1166 | { 1167 | $url = HTTP::ExtractURL($this->info["action"]); 1168 | unset($url["query"]); 1169 | $url["queryvars"] = $fields; 1170 | $result = array( 1171 | "url" => HTTP::CondenseURL($url), 1172 | "options" => array() 1173 | ); 1174 | } 1175 | else 1176 | { 1177 | $result = array( 1178 | "url" => $this->info["action"], 1179 | "options" => array( 1180 | "postvars" => $fields, 1181 | "files" => $files 1182 | ) 1183 | ); 1184 | } 1185 | 1186 | return $result; 1187 | } 1188 | } 1189 | ?> --------------------------------------------------------------------------------