├── README.md ├── docs ├── crc32_stream.md ├── deflate_stream.md ├── doh_web_browser.md ├── emulate_curl.md ├── http.md ├── multi_async_helper.md ├── tag_filter.md ├── web_browser.md ├── web_server.md ├── websocket.md ├── websocket_server.md └── websocket_server_libev.md ├── doh_web_browser.php ├── offline_download_example.php ├── support ├── cacert.pem ├── crc32_stream.php ├── deflate_stream.php ├── emulate_curl.php ├── http.php ├── multi_async_helper.php ├── simple_html_dom.php ├── tag_filter.php ├── utf_utils.php ├── web_browser.php └── websocket.php ├── tests ├── test_dns_over_https.php ├── test_interactive.php ├── test_suite.php ├── test_web_server.php ├── test_websocket_client.php ├── test_websocket_server.php ├── test_word.txt └── test_xss.txt ├── web_server.php ├── websocket_server.php └── websocket_server_libev.php /docs/crc32_stream.md: -------------------------------------------------------------------------------- 1 | CRC32Stream Class: 'support/crc32_stream.php' 2 | ============================================== 3 | 4 | This class calculates any CRC32 checksum (any polynomial/reflection combo) in a stream-enabled class. It is a direct port of the CubicleSoft C++ implementation of the same name. 5 | 6 | A Cyclic Redundancy Check, or CRC, is used for detecting if an error exists somewhere in a data stream, but generally doesn't provide a means of correcting that error. CRC32Stream is used by the DeflateStream class to validate incoming data for compression/decompression and can handle an unlimited amount of data due to the streaming capabilities (unlike the built-in and slightly buggy crc32 function in PHP). 7 | 8 | This class will internally use the PHP Hash extension in 'crc32b' mode if available AND the defaults are used for the Init() call. Otherwise, it falls back to a less-performant userland implementation. 9 | 10 | Example usage: 11 | 12 | ```php 13 | Init(); 20 | $crc->AddData($data); 21 | $crc->AddData($data2); 22 | $result = $crc->Finalize(); 23 | 24 | echo sprintf("%u\n", $result); 25 | ?> 26 | ``` 27 | 28 | CRC32Stream::Init($options = false) 29 | ----------------------------------- 30 | 31 | Access: public 32 | 33 | Parameters: 34 | 35 | * $options - An optional array of information or a boolean of false (Default = false). 36 | 37 | Returns: Nothing. 38 | 39 | This function initializes the class. The class can be reused by calling Init(). The $options array must have key-value pairs as follows: 40 | 41 | * poly - An integer for the polynomial. 42 | * start - An integer for the starting value. 43 | * xor - An integer for the XOR value. 44 | * refdata - A boolean that indicates whether or not to reflect the data. 45 | * refcrc - A boolean that indicates whether or not to reflect the final CRC value. 46 | 47 | The most popular and widely used CRC32 values are used if false is passed in as the value for $options, which is the default. In general, the default is probably what you want to use. 48 | 49 | CRC32Stream::AddData($data) 50 | --------------------------- 51 | 52 | Access: public 53 | 54 | Parameters: 55 | 56 | * $data - A string containing the data to process for the CRC. 57 | 58 | Returns: A boolean of true if Finalize() has not been called, false otherwise. 59 | 60 | This function processes the data and updates the internal CRC state. 61 | 62 | CRC32Stream::Finalize() 63 | ----------------------- 64 | 65 | Access: public 66 | 67 | Parameters: None. 68 | 69 | Returns: An integer containing the final CRC value. 70 | 71 | This function finalizes the CRC state and return the value. To use the class again after calling this function, call Init(). 72 | 73 | CRC32Stream::SHR32($num, $bits) 74 | ------------------------------- 75 | 76 | Access: private 77 | 78 | Parameters: 79 | 80 | * $num - A 32-bit integer to shift right. 81 | * $bits - The number of bits to shift. 82 | 83 | Returns: A valid 32-bit integer shifted right the specified number of bits. 84 | 85 | This internal function shifts an integer to the right the specified number of bits across all hardware that PHP runs on. 86 | 87 | CRC32Stream::SHL32($num, $bits) 88 | ------------------------------- 89 | 90 | Access: private 91 | 92 | Parameters: 93 | 94 | * $num - A 32-bit integer to shift left. 95 | * $bits - The number of bits to shift. 96 | 97 | Returns: A valid 32-bit integer shifted right the specified number of bits. 98 | 99 | This internal function shifts an integer to the left the specified number of bits across all hardware that PHP runs on. 100 | 101 | CRC32Stream::LIM32($num) 102 | ------------------------ 103 | 104 | Access: private 105 | 106 | Parameters: 107 | 108 | * $num - A 32-bit integer to cap to 32-bits. 109 | 110 | Returns: A valid 32-bit integer. 111 | 112 | This internal function is used by other functions in the class to cap 32-bit integers on all hardware that PHP runs on. 113 | -------------------------------------------------------------------------------- /docs/deflate_stream.md: -------------------------------------------------------------------------------- 1 | DeflateStream Class: 'support/deflate_stream.php' 2 | ================================================== 3 | 4 | This class implements three compression/extraction algorithms in wide use on web servers in a stream-enabled format: RFC1951 (raw deflate - default), RFC1950 (ZLIB), and RFC1952 (gzip). 5 | 6 | This class works entirely using RAM. Most of the compression routines in PHP require using an external temporary file to compress and uncompress data. On the downside, the RAM only aspect of this class does make PHP more suceptible to significant issues such as ZIP bombs. 7 | 8 | The PHP functions stream_filter_append(), stream_filter_remove(), and gzcompress() are required to exist for this class to work. Any modern version of PHP compiled with zlib support should work. The CRC32Stream class is also required for gzip streams. 9 | 10 | Example usage: 11 | 12 | ```php 13 | Init("wb"); 34 | $ds->Write($data); 35 | $ds->Finalize(); 36 | $data = $ds->Read(); 37 | 38 | $ds->Init("rb", -1, array("type" => "auto")); 39 | $ds->Write($data); 40 | $ds->Finalize(); 41 | $data = $ds->Read(); 42 | echo $data . "\n"; 43 | ?> 44 | ``` 45 | 46 | DeflateStream::IsSupported() 47 | ---------------------------- 48 | 49 | Access: public static 50 | 51 | Parameters: None. 52 | 53 | Returns: A boolean of true if DeflateStream is supported, false otherwise. 54 | 55 | This static function should be used to determine if DeflateStream will work before attempting to use the class. 56 | 57 | DeflateStream::Compress($data, $compresslevel = -1, $options = array()) 58 | ----------------------------------------------------------------------- 59 | 60 | Access: public static 61 | 62 | Parameters: 63 | 64 | * $data - The string to compress. 65 | * $compresslevel - An integer representing the compression level to use (Default is -1). 66 | * $options - An array of options to use for the Init() call (Default is array()). 67 | 68 | Returns: A string containing the compressed data on success, a boolean of false otherwise. 69 | 70 | This static function compresses a string completely in memory at once. See Init() for a list of valid options for the $options array. 71 | 72 | DeflateStream::Uncompress($data, $options = array("type" => "auto")) 73 | -------------------------------------------------------------------- 74 | 75 | Access: public static 76 | 77 | Parameters: 78 | 79 | * $data - The string to compress. 80 | * $options - An array of options to use for the Init() call (Default is array("type" => "auto")). 81 | 82 | Returns: A string containing the compressed data on success, a boolean of false otherwise. 83 | 84 | This static function uncompresses a string completely in memory at once. See Init() for a list of valid options for the $options array. 85 | 86 | DeflateStream::Init($mode, $compresslevel = -1, $options = array()) 87 | ------------------------------------------------------------------- 88 | 89 | Access: public 90 | 91 | Parameters: 92 | 93 | * $mode - A string of either "wb" to compress or "rb" to uncompress. 94 | * $compresslevel - An integer representing the compression level to use (Default is -1). 95 | * $options - An array of options (Default is array()). 96 | 97 | Returns: A boolean of true if initialization was successful, false otherwise. 98 | 99 | This function initializes the class to either compress or uncompress written data. The $compresslevel is dependent upon the algorithm but -1 (the default) usually strikes a good balance between performance and size when compressing data. The $options array can currently accept the following key-value pair: 100 | 101 | * type - A string of one of "rfc1951", "raw", "rfc1950", "zlib", "rfc1952", "gzip", and "auto" (Default is "raw"). The "auto" option is only valid when uncompressing data. 102 | 103 | DeflateStream::Write($data) 104 | --------------------------- 105 | 106 | Access: public 107 | 108 | Parameters: 109 | 110 | * $data - The string to process. 111 | 112 | Returns: A boolean of true if the data was processed successfully, false otherwise. 113 | 114 | This function processes incoming data and moves any available compressed/uncompressed results into the output area for reading. For performance reasons, as much data as possible should be sent to Write() at one time. Depending on the mode, not all data may be sent at once. Note that just because data goes in doesn't mean it will result in any output when Read() is called. 115 | 116 | DeflateStream::Read() 117 | --------------------- 118 | 119 | Access: public 120 | 121 | Parameters: None. 122 | 123 | Returns: A string containing any available output. 124 | 125 | This function returns the available output from the previous Write() call and also clears the internal output. 126 | 127 | DeflateStream::Finalize() 128 | ------------------------- 129 | 130 | Access: public 131 | 132 | Parameters: None. 133 | 134 | Returns: A boolean of true if finalization succeeded, false otherwise. 135 | 136 | This function flushes all internal data streams and finalizes the output for the last Read() call. After Finalize() is called, no more data may be written but there will likely be data to be read. 137 | 138 | DeflateStream::ProcessOutput() 139 | ------------------------------ 140 | 141 | Access: private 142 | 143 | Parameters: None. 144 | 145 | Returns: Nothing. 146 | 147 | This internal function extracts the data from the output end of things and stores it for a future Read() call. The function exists mostly because PHP is broken when working with ftell() and PHP filters. See: https://bugs.php.net/bug.php?id=49874 148 | 149 | DeflateStream::ProcessInput($final = false) 150 | ------------------------------------------- 151 | 152 | Access: private 153 | 154 | Parameters: 155 | 156 | * $final - A boolean that indicates whether or not this is the last input (Default is false). 157 | 158 | Returns: Nothing. 159 | 160 | This internal function processes data made available as a result of calling the Write() and Finalize() functions. 161 | 162 | DeflateStream::SHL32($num, $bits) 163 | --------------------------------- 164 | 165 | Access: private 166 | 167 | Parameters: 168 | 169 | * $num - A 32-bit integer to shift left. 170 | * $bits - The number of bits to shift. 171 | 172 | Returns: A valid 32-bit integer shifted right the specified number of bits. 173 | 174 | This internal function shifts an integer to the left the specified number of bits across all hardware that PHP runs on. 175 | 176 | DeflateStream::LIM32($num) 177 | -------------------------- 178 | 179 | Access: private 180 | 181 | Parameters: 182 | 183 | * $num - A 32-bit integer to cap to 32-bits. 184 | 185 | Returns: A valid 32-bit integer. 186 | 187 | This internal function is used by other functions in the class to cap 32-bit integers on all hardware that PHP runs on. 188 | 189 | DeflateStream::ADD32($num, $num2) 190 | --------------------------------- 191 | 192 | Access: private 193 | 194 | Parameters: 195 | 196 | * $num - A 32-bit integer. 197 | * $num2 - Another 32-bit integer. 198 | 199 | Returns: The 32-bit capped result of adding two 32-bit integers together. 200 | 201 | This internal function adds two 32-bit integers together on all hardware that PHP runs on. 202 | -------------------------------------------------------------------------------- /docs/doh_web_browser.md: -------------------------------------------------------------------------------- 1 | DOHWebBrowser Class: 'support/doh_web_browser.php' 2 | =================================================== 3 | 4 | The DNS Over HTTPS (DOH) web browser class integrates with DOH servers to attempt to provide more secure responses to DNS queries than standard DNS resolvers. This drop-in replacement class for WebBrowser overrides some aspects of the WebBrowser class to provide transparent DOH support. 5 | 6 | By default, the class uses the [Cloudflare DOH resolver](https://developers.cloudflare.com/1.1.1.1/dns-over-https/request-structure) to rewrite each web request. 7 | 8 | Note that this class is not asynchronous/non-blocking except if a host IP has already been resolved and stored in the cache in RAM. 9 | 10 | DOHWebBrowser::SetDOHAccessInfo($dohapi, $dohhost = false, $dohtypes = array("A", "AAAA")) 11 | ------------------------------------------------------------------------------------------ 12 | 13 | Access: public 14 | 15 | Parameters: 16 | 17 | * $dohapi - A string containing the DNS Over HTTPS API to use. Must be compatible with the Cloudflare DNS JSON API. 18 | * $dohhost- A string containing the DNS Over HTTPS host to use (Default is false). Use if `$dohapi` contains an IP address. 19 | * $dohtypes - An array containing the DNS types to query in order of preference (Default is array("A", "AAAA")). 20 | 21 | Returns: Nothing. 22 | 23 | This function sets the DOH access information to use when resolving DNS queries. 24 | 25 | DOHWebBrowser::ClearDOHCache() 26 | ------------------------------ 27 | 28 | Access: public 29 | 30 | Parameters: None. 31 | 32 | Returns: Nothing. 33 | 34 | This function clears/flushes the shared internal resolver cache of all entries. 35 | 36 | DOHWebBrowser::GetDOHCache() 37 | ---------------------------- 38 | 39 | Access: public 40 | 41 | Parameters: None. 42 | 43 | Returns: An array containing the shared internal resolver cache entries. 44 | 45 | This function returns the internal resolver cache entries. These cached entries are shared across all DOHWebBrowser class instances. 46 | 47 | DOHWebBrowser::InternalDNSOverHTTPSHandler(&$state) 48 | --------------------------------------------------- 49 | 50 | Access: public 51 | 52 | Parameters: 53 | 54 | * $state - An array containing state information. 55 | 56 | Returns: A boolean that determines whether or not the request should continue. 57 | 58 | This internal function is the workhorse of the class. It connects to the DOH resolver as needed, runs queries, alters the original request using the resolver response, and then lets the WebBrowser class continue on using the modified information. 59 | -------------------------------------------------------------------------------- /docs/emulate_curl.md: -------------------------------------------------------------------------------- 1 | cURL Emulation Layer: 'support/emulate_curl.php' 2 | ================================================= 3 | 4 | If you have a web host that doesn't support cURL but need to run a piece of software that requires cURL to function properly, the cURL emulation layer allows you to easily and quickly make most applications think that cURL is available. Put simply, the cURL emulation layer is a drop-in replacement for cURL on web hosts that don't have cURL installed. Somewhere early on in the execution path (e.g. an 'index.php' file), just include the cURL emulation layer and it will handle the rest. 5 | 6 | Every define() and function is available as of PHP 5.4.0. 7 | 8 | However, there are a few limitations and differences. CURLOPT_VERBOSE is a lot more verbose. SSL/TLS support is a little flaky at times. Some things like DNS options are ignored. Only HTTP and HTTPS are supported protocols at this time. Return values from curl_getinfo() calls are close but not the same. curl_setopt() delays processing until curl_exec() is called. Multi-handle support "cheats" by performing operations in linear execution rather than parallel execution. 9 | 10 | Example usage: 11 | 12 | ```php 13 | 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/multi_async_helper.md: -------------------------------------------------------------------------------- 1 | MultiAsyncHelper Class: 'support/multi_async_helper.php' 2 | ========================================================= 3 | 4 | Asynchronous, or non-blocking, sockets allow for a lot of powerful functionality such as scraping multiple pages and sites simultaneously from a single script. However, management of the entire process can result in a lot of extra code. This class manages the nitty-gritty details of queueing up and simultaneously retrieving content from multiple URLs. It is a powerful class, though, which means it can be used for other I/O related things besides sockets (e.g. files). 5 | 6 | Using MultiAsyncHelper for bulk web scraping tasks is NOT recommended. Running concurrent requests to the same destination network has a much higher likelihood of getting noticed by a system administrator and result in an IP address block. 7 | 8 | Example usage: 9 | 10 | ```php 11 | SetConcurrencyLimit(3); 26 | 27 | // Add the URLs to the async helper. The WebBrowser class knows how to correctly queue up a fully-managed request. 28 | $pages = array(); 29 | foreach ($urls as $url) 30 | { 31 | $key = $url; 32 | 33 | $pages[$key] = new WebBrowser(); 34 | 35 | // Definition: public function WebBrowser::ProcessAsync($helper, $key, $callback, $url, $tempoptions = array()) 36 | $pages[$key]->ProcessAsync($helper, $key, NULL, $url); 37 | } 38 | 39 | // Run the main loop. 40 | $result = $helper->Wait(); 41 | while ($result["success"]) 42 | { 43 | // Process finished pages. 44 | foreach ($result["removed"] as $key => $info) 45 | { 46 | if (!$info["result"]["success"]) echo "Error retrieving URL (" . $key . "). " . $info["result"]["error"] . "\n"; 47 | else if ($info["result"]["response"]["code"] != 200) echo "Error retrieving URL (" . $key . "). Server returned: " . $info["result"]["response"]["line"] . "\n"; 48 | else 49 | { 50 | echo "A response was returned (" . $key . ").\n"; 51 | 52 | // Do something with the data here... 53 | } 54 | 55 | unset($pages[$key]); 56 | } 57 | 58 | // Break out of the loop when there is nothing left to do. 59 | if (!$helper->NumObjects()) break; 60 | 61 | $result = $helper->Wait(); 62 | } 63 | 64 | // An error occurred. 65 | if (!$result["success"]) var_dump($result); 66 | ?> 67 | ``` 68 | 69 | MultiAsyncHelper::SetConcurrencyLimit($limit) 70 | --------------------------------------------- 71 | 72 | Access: public 73 | 74 | Parameters: 75 | 76 | * $limit - An integer that specifies the maximum number of running/active items at one time or a boolean of false for no limit. 77 | 78 | Returns: Nothing. 79 | 80 | This function sets the maximum number of running/active items. Items in the queue are automatically moved to the active state whenever space is freed up. Note that changing this has no impact on existing items that are already in the active state. 81 | 82 | MultiAsyncHelper::Set($key, $obj, $callback) 83 | -------------------------------------------- 84 | 85 | Access: public 86 | 87 | Parameters: 88 | 89 | * $key - A string representing the key to use to identify $obj in the future. 90 | * $obj - A valid stream_select() compatible I/O object. This can be a socket, a file, or anything else that PHP supports. 91 | * $callback - A string or an array to a callback function to call whenever the mode changes in relation to the object. The callback must specify four parameters - callback($mode, &$data, $key, $fp). 92 | 93 | Returns: Nothing. 94 | 95 | This function will place the specified object in the internal queue and associate it with the specified key. If this function is called in the future with an identical key, any existing object with the same key still in the class will be removed and the new object will be placed in the queue. 96 | 97 | MultiAsyncHelper::NumObjects() 98 | ------------------------------ 99 | 100 | Access: public 101 | 102 | Parameters: None. 103 | 104 | Returns: The number of objects in the helper. 105 | 106 | This function returns the number queued and active objects currently managed by this class instance. 107 | 108 | MultiAsyncHelper::GetObject($key) 109 | --------------------------------- 110 | 111 | Access: public 112 | 113 | Parameters: 114 | 115 | * $key - A string representing the key associated with an object. 116 | 117 | Returns: The associated object if it still exists, a boolean of false otherwise. 118 | 119 | This function retrieves the object associated with the specified key. 120 | 121 | MultiAsyncHelper::SetCallback($key, $callback) 122 | ---------------------------------------------- 123 | 124 | Access: public 125 | 126 | Parameters: 127 | 128 | * $key - A string representing the key associated with an object. 129 | * $callback - A string or an array to a callback function to call whenever the mode changes in relation to the object. The new callback must specify four parameters - callback($mode, &$data, $key, $fp). 130 | 131 | Returns: Nothing. 132 | 133 | This function replaces the existing callback associated with the object with the new callback. This could be used, for example, to replace a read-only callback with a write-only callback to send a response to a request. However, it may be simpler to use a single callback that can correctly manage state. 134 | 135 | MultiAsyncHelper::Detach($key) 136 | ------------------------------ 137 | 138 | Access: public 139 | 140 | Parameters: 141 | 142 | * $key - A string representing the key associated with an object. 143 | 144 | Returns: The detached object if successful, a boolean of false otherwise. 145 | 146 | This function detaches the object from the internal structures. The associated callback is called with a $mode of "cleanup" and $data is false. The callback should perform whatever actions are necessary to pause operations (if any). 147 | 148 | MultiAsyncHelper::Remove($key) 149 | ------------------------------ 150 | 151 | Access: public 152 | 153 | Parameters: 154 | 155 | * $key - A string representing the key associated with an object. 156 | 157 | Returns: The removed object if successful, a boolean of false otherwise. 158 | 159 | This function detaches the object from the internal structures. The associated callback is called with a $mode of "cleanup" and $data is true. The callback should perform whatever actions are necessary to end all operations with the object. Normally, you won't need to call this unless you want to perform some sort of timeout if an object is unresponsive for too long. 160 | 161 | MultiAsyncHelper::Wait($timeout = false) 162 | ---------------------------------------- 163 | 164 | Access: public 165 | 166 | Parameters: 167 | 168 | * $timeout - An integer representing the maximum number of seconds to wait before continuing on anyway or a boolean of false to wait indefinitely (Default is false). 169 | 170 | Returns: A standard array of information. 171 | 172 | This function moves waiting items in the queue to the active state up to the concurrency limit, runs callbacks to process the active queue items, waits for up to the specified timeout period for something to happen via stream_select(), and returns the result of the operation. The response can include many items or no items to work on. 173 | 174 | MultiAsyncHelper::ReadOnly($mode, &$data, $key, $fp) 175 | ---------------------------------------------------- 176 | 177 | Access: public static 178 | 179 | Parameters: 180 | 181 | * $mode - A string representing the mode/state to process. 182 | * $data - Mixed content the depends entirely on the $mode. 183 | * $key - A string representing the key associated with an object. 184 | * $fp - The object associated with the key. 185 | 186 | Returns: Nothing. 187 | 188 | This static callback function is intended for use with direct non-blocking file/socket handles. Using this function as the callback will cause Wait() to only wait for readability on the object. 189 | 190 | MultiAsyncHelper::WriteOnly($mode, &$data, $key, $fp) 191 | ----------------------------------------------------- 192 | 193 | Access: public static 194 | 195 | Parameters: 196 | 197 | * $mode - A string representing the mode/state to process. 198 | * $data - Mixed content the depends entirely on the $mode. 199 | * $key - A string representing the key associated with an object. 200 | * $fp - The object associated with the key. 201 | 202 | Returns: Nothing. 203 | 204 | This static callback function is intended for use with direct non-blocking file/socket handles. Using this function as the callback will cause Wait() to only wait for writability on the object. 205 | 206 | MultiAsyncHelper::ReadAndWrite($mode, &$data, $key, $fp) 207 | -------------------------------------------------------- 208 | 209 | Access: public static 210 | 211 | Parameters: 212 | 213 | * $mode - A string representing the mode/state to process. 214 | * $data - Mixed content the depends entirely on the $mode. 215 | * $key - A string representing the key associated with an object. 216 | * $fp - The object associated with the key. 217 | 218 | Returns: Nothing. 219 | 220 | This static callback function is intended for use with direct non-blocking file/socket handles. Using this function as the callback will cause Wait() to only wait for either readability or writability on the object, whichever happens first (or both). 221 | 222 | MultiAsyncHelper::InternalDetach($key, $cleanup) 223 | ------------------------------------------------ 224 | 225 | Access: private 226 | 227 | Parameters: 228 | 229 | * $key - A string representing the key associated with an object. 230 | * $cleanup - A boolean indicating the value to pass to $data in the callback associated with the object. 231 | 232 | Returns: The removed object if successful, a boolean of false otherwise. 233 | 234 | This internal function is called by Detach() and Remove(). 235 | 236 | MultiAsyncHelper::MAHTranslate($format, ...) 237 | --------------------------------- 238 | 239 | Access: _internal_ static 240 | 241 | Parameters: 242 | 243 | * $format - A string containing valid sprintf() format specifiers. 244 | 245 | Returns: A string containing a translation. 246 | 247 | 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. 248 | -------------------------------------------------------------------------------- /docs/websocket.md: -------------------------------------------------------------------------------- 1 | WebSocket Class: 'support/websocket.php' 2 | ========================================= 3 | 4 | This class provides client-side routines to communicate with a WebSocket server (RFC 6455) and enable live scraping of data from such servers. The WebSocket class allows a web scraping application to bring in data immediately as soon as it becomes available. 5 | 6 | Example usage: 7 | 8 | ```php 9 | Connect("ws://ws.something.org/", "http://www.something.org"); 17 | if (!$result["success"]) 18 | { 19 | var_dump($result); 20 | exit(); 21 | } 22 | 23 | // Send a text frame (just an example). 24 | $result = $ws->Write("Testtext", WebSocket::FRAMETYPE_TEXT); 25 | 26 | // Send a binary frame (just an example). 27 | $result = $ws->Write("Testbinary", WebSocket::FRAMETYPE_BINARY); 28 | 29 | // Main loop. 30 | do 31 | { 32 | $result = $ws->Wait(); 33 | if (!$result["success"]) break; 34 | 35 | do 36 | { 37 | $result = $ws->Read(); 38 | if (!$result["success"]) break; 39 | if ($result["data"] !== false) 40 | { 41 | // Do something with the data. 42 | var_dump($result["data"]); 43 | } 44 | } while ($result["data"] !== false); 45 | } while (1); 46 | 47 | // An error occurred. 48 | var_dump($result); 49 | ?> 50 | ``` 51 | 52 | Another example: 53 | 54 | https://github.com/cubiclesoft/ultimate-web-scraper/blob/master/tests/test_websocket_client.php 55 | 56 | The WebSocket class manages two queues - a read queue and a write queue - and does most of its work in the `Wait()` function. WebSocket control frames are automatically handled by the WebSocket class. 57 | 58 | The `Connect()` and `Disconnect()` functions are blocking in client mode, non-blocking in server mode. 59 | 60 | WebSocket::Reset() 61 | ------------------ 62 | 63 | Access: public 64 | 65 | Parameters: None. 66 | 67 | Returns: Nothing. 68 | 69 | This function resets a class instance to the default state. Note that Disconnect() should be called first as this simply resets all internal class variables. 70 | 71 | WebSocket::SetServerMode() 72 | -------------------------- 73 | 74 | Access: public 75 | 76 | Parameters: None. 77 | 78 | Returns: Nothing. 79 | 80 | This function switches the client to server mode. Prefer using the WebSocketServer class for implementing an actual WebSocket server. 81 | 82 | WebSocket::SetClientMode() 83 | -------------------------- 84 | 85 | Access: public 86 | 87 | Parameters: None. 88 | 89 | Returns: Nothing. 90 | 91 | This function switches to client mode. This is the default mode. 92 | 93 | WebSocket::SetExtensions($extensions) 94 | ------------------------------------- 95 | 96 | Access: public 97 | 98 | Parameters: 99 | 100 | * $extensions - An array containing a list of extensions to support. 101 | 102 | Returns: Nothing. 103 | 104 | This function replaces the internal extensions variable. It currently has no effect but is intended to be used to support whatever IETF standards track extensions are eventually invented (if any). The "permessage-deflate" extension seems to be headed this route. 105 | 106 | WebSocket::SetCloseMode($mode) 107 | ------------------------------ 108 | 109 | Access: public 110 | 111 | Parameters: 112 | 113 | * $mode - One of the following constants: 114 | * WebSocket::CLOSE_IMMEDIATELY (default mode) 115 | * WebSocket::CLOSE_AFTER_CURRENT_MESSAGE 116 | * WebSocket::CLOSE_AFTER_ALL_MESSAGES 117 | 118 | Returns: Nothing. 119 | 120 | This function sets the behavior for handling the close frame. The default is to send the close frame immediately and/or terminate the connection. Both sides are supposed to receive the close frame before closing the connection. 121 | 122 | WebSocket::SetKeepAliveTimeout($keepalive) 123 | ------------------------------------------ 124 | 125 | Access: public 126 | 127 | Parameters: 128 | 129 | * $keepalive - An integer specifying the number of seconds since the last packet received before sending a ping packet. 130 | 131 | Returns: Nothing. 132 | 133 | This function sets the timeout interval. The default is 30 seconds. Whenever a packet is received, the timer resets. If the timeout is reached, first a ping packet is sent. If nothing is received for another timeout period, neither a pong response packet nor other packet, the connection will self-terminate. 134 | 135 | WebSocket::GetKeepAliveTimeout() 136 | -------------------------------- 137 | 138 | Access: public 139 | 140 | Parameters: None. 141 | 142 | Returns: An integer specifying the current keep alive timeout period. 143 | 144 | This function returns the internal keep alive timeout value. 145 | 146 | WebSocket::SetMaxReadFrameSize($maxsize) 147 | ---------------------------------------- 148 | 149 | Access: public 150 | 151 | Parameters: 152 | 153 | * $maxsize - A boolean of false to indicate unlimited size or an integer specifying the maximum number of bytes for single frame payload. 154 | 155 | Returns: Nothing. 156 | 157 | This function sets the limit on received frame size. Without it, a malicious client or server could potentially consume all available system resources. The default is 2000000 (2 million) bytes. This setting has no effect on the maximum received message size since multiple frames can make up a complete message. This also helps keeps individual frame size down to a reasonable limit. 158 | 159 | WebSocket::SetMaxReadMessageSize($maxsize) 160 | ------------------------------------------ 161 | 162 | Access: public 163 | 164 | Parameters: 165 | 166 | * $maxsize - A boolean of false to indicate unlimited size or an integer specifying the maximum number of bytes for single message payload. 167 | 168 | Returns: Nothing. 169 | 170 | This function sets the limit on message size. Without it, a malicious client or server could potentially consume all available system resources. The default is 10000000 (10 million) bytes. 171 | 172 | WebSocket::GetRawRecvSize() 173 | --------------------------- 174 | 175 | Access: public 176 | 177 | Parameters: None. 178 | 179 | Returns: An integer containing the total number of bytes received. 180 | 181 | This function retrieves the total number of bytes received, which includes frame bytes. 182 | 183 | WebSocket::GetRawSendSize() 184 | --------------------------- 185 | 186 | Access: public 187 | 188 | Parameters: None. 189 | 190 | Returns: An integer containing the total number of bytes sent. 191 | 192 | This function retrieves the total number of bytes sent, which includes frame bytes. 193 | 194 | WebSocket::Connect($url, $origin, $options = array(), $web = false) 195 | ------------------------------------------------------------------- 196 | 197 | Access: public 198 | 199 | Parameters: 200 | 201 | * $url - A string containing a WebSocket URL (starts with ws:// or wss://). 202 | * $origin - A string containing an Origin URL. 203 | * $options - An array of valid WebBrowser class options (Default is array()). 204 | * $web - A valid WebBrowser class instance (default is false, which means one will be created). 205 | 206 | Returns: An array containing the results of the call. 207 | 208 | This function initiates a connection to a WebSocket 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 it for the connection. 209 | 210 | $origin is a required parameter because servers expect only web browsers to connect to WebSocket servers and the specification (RFC 6455) clearly states that only automated processes will not send the Origin header but web browsers must do so. Sending the Origin header is necessary to mimic a web browser. 211 | 212 | WebSocket::Disconnect() 213 | ----------------------- 214 | 215 | Access: public 216 | 217 | Parameters: None. 218 | 219 | Returns: Nothing. 220 | 221 | This function disconnects an active connection after sending/receiving the close frame (clean shutdown) and resets a few internal variables in case the class is reused. Note that it is better to call Disconnect() instead of letting the destructor handle shutting down the connection so that a graceful shutdown may take place. 222 | 223 | WebSocket::Read($finished = true, $wait = false) 224 | ------------------------------------------------ 225 | 226 | Access: public 227 | 228 | Parameters: 229 | 230 | * $finished - A boolean indicating whether or not to return finished messages (default is true). 231 | * $wait - A boolean indicating whether or not to wait until a message has arrived that meets the rest of the function call criteria (default is false). 232 | 233 | Returns: An array containing the results of the call. 234 | 235 | This function performs an asynchronous read operation on the message queue (except if $wait is true). When $finished is false, the returned message may be a fragment, which can be useful if there is a large amount of data flowing in for a single message. 236 | 237 | It is a good idea to clear out the read queue at every opportunity. WebSocket servers tend to push lots of messages, so multiple messages may queue up. Always call Read() multiple times until there are no more messages before doing another Wait(). 238 | 239 | WebSocket::Write($message, $frametype, $last = true, $wait = false) 240 | ------------------------------------------------------------------- 241 | 242 | Access: public 243 | 244 | Parameters: 245 | 246 | * $message - A string containing the message to send. 247 | * $frametype - One of the following constants: 248 | * WebSocket::FRAMETYPE_TEXT - A UTF-8 text frame. 249 | * WebSocket::FRAMETYPE_BINARY - A binary frame. 250 | * WebSocket::FRAMETYPE_CONNECTION_CLOSE - To close the connection. Call Disconnect() instead. 251 | * WebSocket::FRAMETYPE_PING - A ping frame. Let WebSocket manage keep-alives. 252 | * WebSocket::FRAMETYPE_PONG - A pong frame. Let WebSocket manage keep-alives. 253 | * $last - A boolean indicating whether or not this is the last fragment of the message (default is true). 254 | * $wait - A boolean indicating whether or not to wait until the write queue has been emptied (default is false). 255 | 256 | Returns: An array containing the results of the call. 257 | 258 | This function sends a single frame. Applications should only ever need to use the first two frame types. The class manages the other three types via other high-level functions such as Wait() and Disconnect(). 259 | 260 | Indicate a fragmented message by using $last. Set $last to false until the last fragment, at which point set it to true (the default). 261 | 262 | WebSocket::NeedsWrite() 263 | ----------------------- 264 | 265 | Access: public 266 | 267 | Parameters: None. 268 | 269 | Returns: A boolean of true if there is data ready to be written to the socket, false otherwise. 270 | 271 | This function transforms up to 65KB of data for writing from the write queue into output frames and returns whether or not there is data for writing. This function can be useful in conjunction with GetStream() when handling multiple streams. 272 | 273 | WebSocket::NumWriteMessages() 274 | ----------------------------- 275 | 276 | Access: public 277 | 278 | Parameters: None. 279 | 280 | Returns: An integer containing the number of messages in the write queue. 281 | 282 | This function returns the number of messages left in the write queue. Note that this does not count bytes or data that has been transformed for writing, just the size of the write queue array. This function can be useful when deciding whether or not to push more data into the write queue to conserve RAM. 283 | 284 | WebSocket::GetStream() 285 | ---------------------- 286 | 287 | Access: public 288 | 289 | Parameters: None. 290 | 291 | Returns: The underlying socket stream (PHP resource) if connected, a boolean of false otherwise. 292 | 293 | This function is considered "dangerous" as it allows direct access to the underlying data 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 WebSocket servers). 294 | 295 | WebSocket::Wait($timeout = false) 296 | --------------------------------- 297 | 298 | Access: public 299 | 300 | Parameters: 301 | 302 | * $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). 303 | 304 | Returns: A standard array of information. 305 | 306 | This function waits until an event occurs such as data arriving, the write end clearing so more data can be sent, or the "nothing has happened for a while, so send a keepalive" timeout. Then WebSocket::ProcessQueuesAndTimeoutState() is called. This function is the core of the WebSocket class and should be called frequently (e.g. a while loop). 307 | 308 | WebSocket::ProcessQueuesAndTimeoutState($read, $write) 309 | ------------------------------------------------------ 310 | 311 | Access: _internal_ 312 | 313 | Parameters: 314 | 315 | * $read - A boolean that indicates that data is available to be read. 316 | * $write - A boolean that indicates that the connection is ready for more data to be written to it. 317 | 318 | Returns: A standard array of information. 319 | 320 | This mostly internal function handles post-Wait() queue processing and a keepalive, if necessary, is queued to be sent. It is declared public so that WebSocketServer can call it to handle the queues and timeout state for an individual client. 321 | 322 | WebSocket::ProcessReadData() 323 | ---------------------------- 324 | 325 | Access: protected 326 | 327 | Parameters: None. 328 | 329 | Returns: An array containing the results of the call. 330 | 331 | This internal function extracts complete frames that have been read in and puts them into the read queue for a later Read() call to handle. Control messages are automatically handled here. 332 | 333 | WebSocket::ReadFrame() 334 | ---------------------- 335 | 336 | Access: protected 337 | 338 | Parameters: None. 339 | 340 | Returns: A boolean of false if there isn't enough data for a complete frame otherwise an array containing the results of the call. 341 | 342 | This internal function attempts to read in the next complete frame from the data that has been read in from the underlying socket. 343 | 344 | WebSocket::FillWriteData() 345 | -------------------------- 346 | 347 | Access: protected 348 | 349 | Parameters: None. 350 | 351 | Returns: Nothing. 352 | 353 | This internal function processes messages to be sent into complete frames until at least 65KB of data is scheduled to be written or all messages have been removed from the queue, whichever comes first. 354 | 355 | WebSocket::WriteFrame($fin, $opcode, $payload) 356 | ---------------------------------------------- 357 | 358 | Access: protected 359 | 360 | Parameters: 361 | 362 | * $fin - A boolean indicating whether or not this is the final frame in a message. 363 | * $opcode - A valid frame type which may also be a continuation frame type. WebSocket automatically determines continuation frames. 364 | * $payload - A string containing part or all of the message to send. 365 | 366 | Returns: Nothing. 367 | 368 | This internal function creates and writes the frame to the internal write data buffer to be sent. 369 | 370 | WebSocket::PRNGBytes($length) 371 | ----------------------------- 372 | 373 | Access: protected 374 | 375 | Parameters: 376 | 377 | * $length - An integer containing the length of the desired output. 378 | 379 | Returns: A string containing $length pseudorandom bytes. 380 | 381 | This internal function follows the RFC 6455 specification for various needs if CSPRNG is available, but it isn't necessary to do so. Rely on WebSocket over SSL (wss://) for actual security. 382 | 383 | WebSocket::UnpackInt($data) 384 | --------------------------- 385 | 386 | Access: _internal_ static 387 | 388 | Parameters: 389 | 390 | * $data - A string containing a big-endian value. 391 | 392 | Returns: An integer or double containing the unpacked value. 393 | 394 | This internal static function is used to process the payload length of incoming frames. 395 | 396 | WebSocket::PackInt64($num) 397 | -------------------------- 398 | 399 | Access: _internal_ static 400 | 401 | Parameters: 402 | 403 | * $num - An integer or double containing the value to pack into an 8 byte string. 404 | 405 | Returns: An 8-byte string containing the packed number. 406 | 407 | This internal function is used to generate the correct payload length for outgoing frames. 408 | 409 | WebSocket::WSTranslate($format, ...) 410 | ------------------------------------ 411 | 412 | Access: _internal_ static 413 | 414 | Parameters: 415 | 416 | * $format - A string containing valid sprintf() format specifiers. 417 | 418 | Returns: A string containing a translation. 419 | 420 | 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. 421 | -------------------------------------------------------------------------------- /docs/websocket_server.md: -------------------------------------------------------------------------------- 1 | WebSocketServer Class: 'support/websocket_server.php' 2 | ====================================================== 3 | 4 | This class is a working WebSocket 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 WebSocket server Upgrade requests to another server. 5 | 6 | Pairs nicely with the WebServer class for handing off Upgrade requests to the WebSocketServer class via the WebSocketServer::ProcessWebServerClientUpgrade() function. 7 | 8 | Be sure to copy "websocket_server.php" into the "support" subdirectory before using it. 9 | 10 | For example basic usage, see: 11 | 12 | https://github.com/cubiclesoft/ultimate-web-scraper/blob/master/tests/test_websocket_server.php 13 | 14 | For a pre-built, flexible, extendable API with user/token management and mostly transparent WebSocket support, see Cloud Storage Server: 15 | 16 | https://github.com/cubiclesoft/cloud-storage-server 17 | 18 | WebSocketServer::Reset() 19 | ------------------------ 20 | 21 | Access: public 22 | 23 | Parameters: None. 24 | 25 | Returns: Nothing. 26 | 27 | This function resets a class instance to the default state. Note that WebSocketServer::Stop() should be called first as this simply resets all internal class variables. 28 | 29 | WebSocketServer::SetWebSocketClass($newclass) 30 | --------------------------------------------- 31 | 32 | Access: public 33 | 34 | Parameters: 35 | 36 | * $newclass - A string containing a valid class name. 37 | 38 | Returns: Nothing. 39 | 40 | This function assigns a new class name of the instance of a class to allocate. The default is "WebSocket" and the specified class must extend WebSocket or there will be problems later on when clients connect in. 41 | 42 | WebSocketServer::SetAllowedOrigins($origins) 43 | -------------------------------------------- 44 | 45 | Access: public 46 | 47 | Parameters: 48 | 49 | * $origins - A string or an array containing allowed Origin header(s) or a boolean of false to allow any Origin header. 50 | 51 | Returns: Nothing. 52 | 53 | This function assigns allowed Origin HTTP header strings. Useful for validating client connections to the WebSocket server when it is public-facing to the Internet or made available via a proxy. Can be spoofed but can prevent XSRF attacks in real web browsers that do send valid Origin header strings. 54 | 55 | WebSocketServer::SetDefaultCloseMode($mode) 56 | ------------------------------------------- 57 | 58 | Access: public 59 | 60 | Parameters: 61 | 62 | * $mode - One of the following constants: 63 | * WebSocket::CLOSE_IMMEDIATELY (default mode) 64 | * WebSocket::CLOSE_AFTER_CURRENT_MESSAGE 65 | * WebSocket::CLOSE_AFTER_ALL_MESSAGES 66 | 67 | Returns: Nothing. 68 | 69 | This function sets the behavior for handling the close frame for all future clients. The default is to send the close frame immediately and/or terminate the connection. Both sides are supposed to receive the close frame before closing the connection. 70 | 71 | WebSocketServer::SetDefaultKeepAliveTimeout($keepalive) 72 | ------------------------------------------------------- 73 | 74 | Access: public 75 | 76 | Parameters: 77 | 78 | * $keepalive - An integer specifying the number of seconds since the last packet received before sending a ping packet. 79 | 80 | Returns: Nothing. 81 | 82 | This function sets the default timeout interval for all future clients. The default is 30 seconds. Whenever a packet is received, the timer resets. If the timeout is reached, first a ping packet is sent. If nothing is received, either a pong response packet or other packet, the connection will self-terminate. 83 | 84 | WebSocketServer::SetDefaultMaxReadFrameSize($maxsize) 85 | ----------------------------------------------------- 86 | 87 | Access: public 88 | 89 | Parameters: 90 | 91 | * $maxsize - A boolean of false to indicate unlimited size or an integer specifying the maximum number of bytes for single frame payload. 92 | 93 | Returns: Nothing. 94 | 95 | This function sets the limit on received frame size for all future clients. Without it, a malicious client or server could potentially consume all available system resources. The default is 2000000 (2 million) bytes. This setting has no effect on the maximum received message size since multiple frames can make up a complete message. This also helps keeps individual frame size down to a reasonable limit. 96 | 97 | WebSocketServer::SetDefaultMaxReadMessageSize($maxsize) 98 | ------------------------------------------------------- 99 | 100 | Access: public 101 | 102 | Parameters: 103 | 104 | * $maxsize - A boolean of false to indicate unlimited size or an integer specifying the maximum number of bytes for single message payload. 105 | 106 | Returns: Nothing. 107 | 108 | This function sets the limit on message size for all future clients. Without it, a malicious client or server could potentially consume all available system resources. The default is 10000000 (10 million) bytes. 109 | 110 | WebSocketServer::Start($host, $port) 111 | ------------------------------------ 112 | 113 | Access: public 114 | 115 | Parameters: 116 | 117 | * $host - A string containing the host IP to bind to. 118 | * $port - A port number to bind to. On some systems, ports under 1024 are restricted to root/admin level access only. 119 | 120 | Returns: An array containing the results of the call. 121 | 122 | This function starts a WebSocket server on the specified host and port. The socket is also set to asynchronous (non-blocking) mode. Some useful values for the host: 123 | 124 | * `0.0.0.0` to bind to all IPv4 interfaces. 125 | * `127.0.0.1` to bind to the localhost IPv4 interface. 126 | * `[::0]` to bind to all IPv6 interfaces. 127 | * `[::1]` to bind to the localhost IPv6 interface. 128 | 129 | To select a new port number for a server, use the following link: 130 | 131 | https://www.random.org/integers/?num=1&min=5001&max=49151&col=5&base=10&format=html&rnd=new 132 | 133 | If it shows port 8080, just reload to get a different port number. 134 | 135 | WebSocketServer::Stop() 136 | ----------------------- 137 | 138 | Access: public 139 | 140 | Parameters: None. 141 | 142 | Returns: Nothing. 143 | 144 | This function stops a WebSocket server after disconnecting all clients and resets some internal variables in case the class instance is reused. 145 | 146 | WebSocketServer::GetStream() 147 | ---------------------------- 148 | 149 | Access: public 150 | 151 | Parameters: None. 152 | 153 | Returns: The underlying socket stream (PHP resource) or a boolean of false if the server is not started. 154 | 155 | 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 WebSocket servers). 156 | 157 | WebSocketServer::UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 158 | ----------------------------------------------------------------------------------- 159 | 160 | Access: public 161 | 162 | Parameters: 163 | 164 | * $prefix - A unique prefix to identify the various streams (server and client handles). 165 | * $timeout - An integer reference containing the maximum number of seconds or a boolean of false. 166 | * $readfps - An array reference to add streams wanting data to arrive. 167 | * $writefps - An array reference to add streams wanting to send data. 168 | 169 | Returns: Nothing. 170 | 171 | This function updates the timeout and read/write arrays with prefixed names so that a single stream_select() call can manage all sockets. 172 | 173 | WebSocketServer::FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 174 | -------------------------------------------------------------------------------- 175 | 176 | Access: public static 177 | 178 | Parameters: Same as stream_select() minus the microsecond parameter. 179 | 180 | Returns: A boolean of true on success, false on failure. 181 | 182 | 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). 183 | 184 | WebSocketServer::Wait($timeout = false) 185 | --------------------------------------- 186 | 187 | Access: public 188 | 189 | Parameters: 190 | 191 | * $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). 192 | 193 | Returns: An array containing the results of the call. 194 | 195 | This function is the core of the WebSocketServer class and should be called frequently (e.g. a while loop). It handles new connections, the initial conversation, basic packet management, 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. 196 | 197 | This function returns an array of clients that were responsive during the call and may have things to do such as Read() incoming messages. 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. 198 | 199 | WebSocketServer::ProcessWaitResult(&$result) 200 | -------------------------------------------- 201 | 202 | Access: protected 203 | 204 | Parameters: 205 | 206 | * $result - An array of standard information containing file handles. 207 | 208 | Returns: Nothing. 209 | 210 | This function processes the result of the Wait() function. Derived classes may call this function (e.g. LibEvWebSocketServer). 211 | 212 | WebSocketServer::ProcessClientQueuesAndTimeoutState(&$result, $id, $read, $write) 213 | --------------------------------------------------------------------------------- 214 | 215 | Access: protected 216 | 217 | Parameters: 218 | 219 | * $result - An array to store the result and/or client information. 220 | * $id - An integer containing an ID of an active client. 221 | * $read - A boolean that indicates that data is available to be read. 222 | * $write - A boolean that indicates that the connection is ready for more data to be written to it. 223 | 224 | Returns: Nothing. 225 | 226 | This internal function calls ProcessQueuesAndTimeoutState() on the specified WebSocket instance. If the call fails, the connection is removed. The results of the call decide where in the result array the client will end up. 227 | 228 | WebSocketServer::GetClients() 229 | ----------------------------- 230 | 231 | Access: public 232 | 233 | Parameters: None. 234 | 235 | Returns: An array of all of the active clients. 236 | 237 | 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 WebSocket protocol. 238 | 239 | WebSocketServer::NumClients() 240 | ----------------------------- 241 | 242 | Access: public 243 | 244 | Parameters: None. 245 | 246 | Returns: The number of active clients. 247 | 248 | This function returns the number clients currently connected to the server. It's more efficient to call this function than to get a copy of the clients array just to `count()` them. 249 | 250 | WebSocketServer::UpdateClientState($id) 251 | --------------------------------------- 252 | 253 | Access: public 254 | 255 | Parameters: 256 | 257 | * $id - An integer containing the ID of the client to update the internal state for. 258 | 259 | Returns: Nothing. 260 | 261 | This function does nothing by default. Derived classes may maintain internal technical state for optimized performance later on (e.g. LibEvWebSocketServer updates read/write notification state for the socket descriptor for use with a later Wait() call). It is recommended that this function be called after calling $ws->Write() on a specific WebSocket. 262 | 263 | WebSocketServer::GetClient($id) 264 | ------------------------------- 265 | 266 | Access: public 267 | 268 | Parameters: 269 | 270 | * $id - An integer containing the ID of the client to retrieve. 271 | 272 | Returns: An array containing client information associated with the ID if it exists, a boolean of false otherwise. 273 | 274 | This function retrieves a single client array by its ID. 275 | 276 | WebSocketServer::RemoveClient($id) 277 | ---------------------------------- 278 | 279 | Access: public 280 | 281 | Parameters: 282 | 283 | * $id - An integer containing the ID of the client to retrieve. 284 | 285 | Returns: Nothing. 286 | 287 | This function terminates a specified client by ID. This is the correct way to disconnect a client. Do not use $client["websocket"]->Disconnect() directly. 288 | 289 | WebSocketServer::ProcessWebServerClientUpgrade($webserver, $client) 290 | ------------------------------------------------------------------- 291 | 292 | Access: public 293 | 294 | Parameters: 295 | 296 | * $webserver - An instance of the WebServer class. 297 | * $client - An instance of WebServer_Client directly associated with the WebServer class. 298 | 299 | Returns: An integer representing the new WebSocketServer client ID on success, false otherwise. 300 | 301 | This function determines if the client is attempting to Upgrade to WebSocket. If so, it detaches the client from the WebServer instance and associates a new client with the WebSocketServer instance. Note that the WebSocketServer instance does not require WebSocketServer::Start() to have been called. 302 | 303 | WebSocketServer::ProcessNewConnection($method, $path, $client) 304 | -------------------------------------------------------------- 305 | 306 | Access: protected 307 | 308 | Parameters: 309 | 310 | * $method - A string containing the HTTP method (supposed to be "GET"). 311 | * $path - A string containing the path portion of the request. 312 | * $client - An array containing introductory information about the new client (parsed headers, etc). 313 | 314 | Returns: A string containing the HTTP response, if any, an empty string otherwise. 315 | 316 | This function handles basic requirements of the WebSocket protocol and will reject obviously bad connections with the appropriate HTTP response string. However, the function can be overridden in a derived class. This class can also call SetWebSocketClass() to switch the class to instantiate just before it is instantiated by the caller. 317 | 318 | WebSocketServer::ProcessAcceptedConnection($method, $path, $client) 319 | ------------------------------------------------------------------- 320 | 321 | Access: protected 322 | 323 | Parameters: 324 | 325 | * $method - A string containing the HTTP method (supposed to be "GET"). 326 | * $path - A string containing the path portion of the request. 327 | * $client - An array containing nearly complete information about the new client (parsed headers, etc). 328 | 329 | Returns: A string containing additional HTTP headers to add to the response, if any, otherwise an empty string. 330 | 331 | 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. 332 | 333 | WebSocketServer::InitNewClient($fp) 334 | ----------------------------------- 335 | 336 | Access: protected 337 | 338 | Parameters: 339 | 340 | * $fp - A stream resource or a boolean of false. 341 | 342 | Returns: The new stdClass instance. 343 | 344 | This function creates a new client object. Since there isn't anything terribly complex about the object, stdClass is used instead of something formal. 345 | 346 | WebSocketServer::ProcessInitialResponse($method, $path, $client) 347 | ---------------------------------------------------------------- 348 | 349 | Access: private 350 | 351 | Parameters: 352 | 353 | * $method - A string containing the HTTP method (supposed to be "GET"). 354 | * $path - A string containing the path portion of the request. 355 | * $client - An array containing nearly complete information about the new client (parsed headers, etc). 356 | 357 | Returns: Nothing. 358 | 359 | This function performs a standard initial response to the client as to whether or not their request to Upgrade to the WebSocket protocol was successful. It also creates the underlying WebSocket client object and sets it to server mode. 360 | 361 | WebSocketServer::HeaderNameCleanup($name) 362 | ----------------------------------------- 363 | 364 | Access: _internal_ static 365 | 366 | Parameters: 367 | 368 | * $name - A string containing a HTTP header name. 369 | 370 | Returns: A string containing a purified HTTP header name. 371 | 372 | This internal static function cleans up a HTTP header name. 373 | 374 | WebSocketServer::WSTranslate($format, ...) 375 | ------------------------------------------ 376 | 377 | Access: _internal_ static 378 | 379 | Parameters: 380 | 381 | * $format - A string containing valid sprintf() format specifiers. 382 | 383 | Returns: A string containing a translation. 384 | 385 | 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. 386 | -------------------------------------------------------------------------------- /docs/websocket_server_libev.md: -------------------------------------------------------------------------------- 1 | LibEvWebSocketServer Class: 'support/websocket_server_libev.php' 2 | ================================================================= 3 | 4 | This class overrides specific functions of WebSocketServer to add PECL ev and libev support. This class is designed to require only minor code changes in order to support PECL ev. 5 | 6 | While the base WebSocketServer class can be used with the WebServer class, using LibEvWebSocketServer with the WebServer class is not recommended at this time. 7 | 8 | For example usage, see [Data Relay Center](https://github.com/cubiclesoft/php-drc). 9 | 10 | LibEvWebSocketServer::IsSupported() 11 | ----------------------------------- 12 | 13 | Access: public static 14 | 15 | Parameters: None. 16 | 17 | Returns: A boolean of true if the PECL ev extension is available and will function on the platform, false otherwise. 18 | 19 | This static function returns whether or not the class will work. Since libev doesn't use I/O Completion Ports (IOCP) on Windows, the function always returns false for PHP on Windows. 20 | 21 | LibEvWebSocketServer::Reset() 22 | ----------------------------- 23 | 24 | Access: public 25 | 26 | Parameters: None. 27 | 28 | Returns: Nothing. 29 | 30 | This function resets a class instance to the default state. Note that LibEvWebSocketServer::Stop() should be called first as this simply resets all internal class variables. 31 | 32 | LibEvWebSocketServer::Internal_LibEvHandleEvent($watcher, $revents) 33 | ------------------------------------------------------------------- 34 | 35 | Access: _internal_ 36 | 37 | Parameters: 38 | 39 | * $watcher - An object containing a PECL ev watcher. 40 | * $revents - An integer containing a set of watcher event flags. 41 | 42 | Returns: Nothing. 43 | 44 | This internal callback function handles PECL ev socket events that are fired. 45 | 46 | LibEvWebSocketServer::Start($host, $port) 47 | ----------------------------------------- 48 | 49 | Access: public 50 | 51 | Parameters: 52 | 53 | * $host - A string containing the host IP to bind to. 54 | * $port - A port number to bind to. On some systems, ports under 1024 are restricted to root/admin level access only. 55 | 56 | Returns: An array containing the results of the call. 57 | 58 | This function starts a WebSocket server on the specified host and port. Identical to WebSocketServer::Start() but also registers for read events on the server socket handle to accept connections. 59 | 60 | LibEvWebSocketServer::Stop() 61 | ---------------------------- 62 | 63 | Access: public 64 | 65 | Parameters: None. 66 | 67 | Returns: Nothing. 68 | 69 | This function stops a WebSocket server after disconnecting all clients and resets some internal variables in case the class instance is reused. Also stops all registered event watchers. 70 | 71 | LibEvWebSocketServer::InitNewClient($fp) 72 | ---------------------------------------- 73 | 74 | Access: protected 75 | 76 | Parameters: 77 | 78 | * $fp - A stream resource or a boolean of false. 79 | 80 | Returns: The new stdClass instance. 81 | 82 | This function creates a new client object. Identical to WebSocketServer::InitNewClient() but also registers for read events on the socket handle. 83 | 84 | LibEvWebSocketServer::UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 85 | ---------------------------------------------------------------------------------------- 86 | 87 | Access: public 88 | 89 | Parameters: 90 | 91 | * $prefix - A unique prefix to identify the various streams (server and client handles). 92 | * $timeout - An integer reference containing the maximum number of seconds or a boolean of false. 93 | * $readfps - An array reference to add streams wanting data to arrive. 94 | * $writefps - An array reference to add streams wanting to send data. 95 | 96 | Returns: Nothing. 97 | 98 | This function updates the timeout and read/write arrays with prefixed names so that a single stream_select() call can manage all sockets. 99 | 100 | Calling this function is not recommended when working with PECL ev. 101 | 102 | LibEvWebSocketServer::Internal_LibEvTimeout($watcher, $revents) 103 | --------------------------------------------------------------- 104 | 105 | Access: _internal_ 106 | 107 | Parameters: 108 | 109 | * $watcher - An object containing a PECL ev watcher. 110 | * $revents - An integer containing a set of watcher event flags. 111 | 112 | Returns: Nothing. 113 | 114 | This internal callback function handles PECL ev timer events that are fired. 115 | 116 | LibEvWebSocketServer::Wait($timeout = false) 117 | -------------------------------------------- 118 | 119 | Access: public 120 | 121 | Parameters: 122 | 123 | * $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). 124 | 125 | Returns: An array containing the results of the call. 126 | 127 | This function is the core of the LibEvWebSocketServer class and should be called frequently (e.g. a while loop). It runs the libev event loop one time and processes WebSocket clients that are returned. 128 | 129 | LibEvWebSocketServer::UpdateClientState($id) 130 | -------------------------------------------- 131 | 132 | Access: public 133 | 134 | Parameters: 135 | 136 | * $id - An integer containing the ID of the client to update the internal state for. 137 | 138 | Returns: Nothing. 139 | 140 | This function updates the watcher for the client for read/write handling during the next Wait() operation. It is recommended that this function be called after calling $ws->Write() on a specific WebSocket. 141 | 142 | LibEvWebSocketServer::RemoveClient($id) 143 | --------------------------------------- 144 | 145 | Access: public 146 | 147 | Parameters: 148 | 149 | * $id - An integer containing the ID of the client to retrieve. 150 | 151 | Returns: Nothing. 152 | 153 | This function terminates a specified client by ID. Identical to WebSocketServer::RemoveClient() and also stops the PECL ev watcher associated with the client. 154 | -------------------------------------------------------------------------------- /doh_web_browser.php: -------------------------------------------------------------------------------- 1 | SetDOHAccessInfo("https://cloudflare-dns.com/dns-query"); 18 | 19 | if (!is_array(self::$dohcache)) self::$dohcache = array(); 20 | } 21 | 22 | public function SetDOHAccessInfo($dohapi, $dohhost = false, $dohtypes = array("A", "AAAA")) 23 | { 24 | $this->dohapi = $dohapi; 25 | $this->dohhost = $dohhost; 26 | $this->dohtypes = $dohtypes; 27 | $this->dohweb = false; 28 | $this->dohfp = false; 29 | } 30 | 31 | public function ClearDOHCache() 32 | { 33 | self::$dohcache = array(); 34 | } 35 | 36 | public function GetDOHCache() 37 | { 38 | return self::$dohcache; 39 | } 40 | 41 | // Override WebBrowser ProcessState(). 42 | public function ProcessState(&$state) 43 | { 44 | if (!isset($state["tempoptions"]["doh_pre_retrievewebpage_callback"])) 45 | { 46 | $state["tempoptions"]["doh_pre_retrievewebpage_callback"] = (isset($state["tempoptions"]["pre_retrievewebpage_callback"]) && is_callable($state["tempoptions"]["pre_retrievewebpage_callback"]) ? $state["tempoptions"]["pre_retrievewebpage_callback"] : false); 47 | 48 | $state["tempoptions"]["pre_retrievewebpage_callback"] = array($this, "InternalDNSOverHTTPSHandler"); 49 | } 50 | 51 | return parent::ProcessState($state); 52 | } 53 | 54 | public function InternalDNSOverHTTPSHandler(&$state) 55 | { 56 | // Skip hosts that appear to be IP addresses. 57 | $host = $state["urlinfo"]["host"]; 58 | if (strpos($host, ":") === false && !preg_match('/^[0-9.]+$/', $host)) 59 | { 60 | if (isset(self::$dohcache[$host]) && self::$dohcache[$host]["expires"] < time()) unset(self::$dohcache[$host]); 61 | 62 | // Obtain the IP address to connect to and cache it for later reuse. 63 | if (!isset(self::$dohcache[$host])) 64 | { 65 | if ($this->dohweb === false) $this->dohweb = new WebBrowser(); 66 | 67 | foreach ($this->dohtypes as $type) 68 | { 69 | $options = array( 70 | "headers" => array( 71 | "Accept" => "application/dns-json", 72 | "Connection" => "Keep-Alive" 73 | ) 74 | ); 75 | 76 | if ($this->dohhost !== false) $options["headers"]["Host"] = $this->dohhost; 77 | if ($this->dohfp !== false) $options["fp"] = $this->dohfp; 78 | 79 | $url2 = $this->dohapi . "?name=" . urlencode($host) . "&type=" . urlencode($type); 80 | 81 | $result = $this->dohweb->Process($url2, $options); 82 | if ($result["success"] && $result["response"]["code"] == 200) 83 | { 84 | if (isset($result["fp"])) $this->dohfp = $result["fp"]; 85 | 86 | $data = @json_decode($result["body"], true); 87 | if (is_array($data) && $data["Status"] == 0 && count($data["Answer"])) 88 | { 89 | self::$dohcache[$host] = $data["Answer"][0]; 90 | self::$dohcache[$host]["expires"] = time() + self::$dohcache[$host]["TTL"]; 91 | self::$dohcache[$host]["stype"] = $type; 92 | 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | 99 | if (!isset(self::$dohcache[$host])) return false; 100 | 101 | $state["options"]["headers"]["Host"] = $state["urlinfo"]["host"]; 102 | $state["urlinfo"]["host"] = self::$dohcache[$host]["data"]; 103 | $state["url"] = HTTP::CondenseURL($state["urlinfo"]); 104 | } 105 | 106 | if ($state["options"]["doh_pre_retrievewebpage_callback"] !== false && !call_user_func_array($state["options"]["doh_pre_retrievewebpage_callback"], array(&$state))) return false; 107 | 108 | return true; 109 | } 110 | } 111 | ?> -------------------------------------------------------------------------------- /offline_download_example.php: -------------------------------------------------------------------------------- 1 | 3 ? (int)$argv[3] : false); 41 | 42 | // Alter input URL to remove potential attack vectors. 43 | $initurl = $argv[2]; 44 | $initurl2 = HTTP::ExtractURL($initurl); 45 | 46 | $initurl2["authority"] = strtolower($initurl2["authority"]); 47 | $initurl2["host"] = strtolower($initurl2["host"]); 48 | if ($initurl2["path"] === "") $initurl2["path"] = "/"; 49 | 50 | $initurl3 = $initurl2; 51 | $initurl3["host"] = ""; 52 | $initurl2["path"] = "/"; 53 | 54 | $initurl = HTTP::ConvertRelativeToAbsoluteURL($initurl2, $initurl3); 55 | 56 | $manifestfile = $destpath . "/" . str_replace(":", "_", $initurl2["authority"]) . "_manifest.json"; 57 | $opsfile = $destpath . "/" . str_replace(":", "_", $initurl2["authority"]) . "_ops_" . md5(($linkdepth === false ? "-1" : $linkdepth) . "|" . $initurl) . ".json"; 58 | 59 | $destpath .= "/" . str_replace(":", "_", $initurl2["authority"]); 60 | @mkdir($destpath, 0770, true); 61 | 62 | $helper = new MultiAsyncHelper(); 63 | $helper->SetConcurrencyLimit(4); 64 | 65 | $htmloptions = TagFilter::GetHTMLOptions(); 66 | $htmloptions["keep_comments"] = true; 67 | 68 | // Provides some basic feedback prior to retrieving each URL. 69 | function DisplayURL(&$state) 70 | { 71 | global $ops; 72 | 73 | echo "[" . number_format(count($ops), 0) . " ops] Retrieving '" . $state["url"] . "'...\n"; 74 | 75 | return true; 76 | } 77 | 78 | // Calculates the static file extension based on the result of a HTTP request. 79 | function GetResultFileExtension(&$result) 80 | { 81 | $mimeextmap = array( 82 | "text/html" => ".html", 83 | "text/plain" => ".txt", 84 | "image/jpeg" => ".jpg", 85 | "image/png" => ".png", 86 | "image/gif" => ".gif", 87 | "text/css" => ".css", 88 | "text/javascript" => ".js", 89 | ); 90 | 91 | // Attempt to map a Content-Type header to a file extension. 92 | if (isset($result["headers"]["Content-Type"])) 93 | { 94 | $header = HTTP::ExtractHeader($result["headers"]["Content-Type"][0]); 95 | 96 | if (isset($mimeextmap[strtolower($header[""])])) return $mimeextmap[$header[""]]; 97 | } 98 | 99 | $fileext = false; 100 | 101 | // Attempt to map a Content-Disposition header to a file extension. 102 | if (isset($result["headers"]["Content-Disposition"])) 103 | { 104 | $header = HTTP::ExtractHeader($result["headers"]["Content-Type"][0]); 105 | 106 | if ($header[""] === "attachment" && isset($header["filename"])) 107 | { 108 | $filename = explode("/", str_replace("\\", "/", $header["filename"])); 109 | $filename = array_pop($filename); 110 | $pos = strrpos($filename, "."); 111 | if ($pos !== false) $fileext = strtolower(substr($filename, $pos)); 112 | } 113 | } 114 | 115 | // Parse the URL and attempt to map to a file extension. 116 | if ($fileext === false) 117 | { 118 | $url = HTTP::ExtractURL($result["url"]); 119 | 120 | $filename = explode("/", str_replace("\\", "/", $url["path"])); 121 | $filename = array_pop($filename); 122 | $pos = strrpos($filename, "."); 123 | if ($pos !== false) $fileext = strtolower(substr($filename, $pos)); 124 | } 125 | 126 | if ($fileext === false) $fileext = ".html"; 127 | 128 | // Avoid unfortunate/accidental local code execution via a localhost web server. 129 | $maptohtml = array( 130 | ".php" => true, 131 | ".php3" => true, 132 | ".php4" => true, 133 | ".php5" => true, 134 | ".php7" => true, 135 | ".phtml" => true, 136 | ".asp" => true, 137 | ".aspx" => true, 138 | ".cfm" => true, 139 | ".jsp" => true, 140 | ".pl" => true, 141 | ".cgi" => true, 142 | ); 143 | 144 | if (isset($maptohtml[$fileext])) $fileext = ".html"; 145 | 146 | return $fileext; 147 | } 148 | 149 | // Attempt to create a roughly-equivalent structure to the URL on the local filesystem for static serving later. 150 | function SetReverseManifestPath($key) 151 | { 152 | global $ops, $opsdata, $initurl2, $manifestrev, $destpath; 153 | 154 | $url2 = HTTP::ExtractURL($key); 155 | $path = ""; 156 | if (strcasecmp($url2["authority"], $initurl2["authority"]) != 0) $path .= "/" . str_replace(":", "_", strtolower($url2["authority"])); 157 | $path .= ($url2["path"] !== "" ? $url2["path"] : "/"); 158 | $path = explode("/", str_replace("\\", "/", TagFilterStream::MakeValidUTF8($path))); 159 | $filename = array_pop($path); 160 | if ($filename === "") $filename = "index"; 161 | 162 | $pos = strrpos($filename, "."); 163 | if ($pos !== false) $filename = substr($filename, 0, $pos); 164 | 165 | if ($url2["query"] !== "") $filename .= "_" . md5($url2["query"]); 166 | 167 | // Make a clean directory. 168 | $vals = $path; 169 | $path = array_shift($vals) . "/"; 170 | while (count($vals)) 171 | { 172 | $path .= array_shift($vals); 173 | 174 | if (isset($manifestrev[strtolower($path)])) $path = $manifestrev[strtolower($path)]; 175 | else $manifestrev[strtolower($path)] = $path; 176 | 177 | $x = 0; 178 | while (is_file($destpath . $path . ($x ? "_" . ($x + 1) : ""))) $x++; 179 | 180 | if ($x) $path .= "_" . ($x + 1); 181 | 182 | $path .= "/"; 183 | } 184 | 185 | @mkdir($destpath . $path, 0770, true); 186 | 187 | // And a clean filename. 188 | $path .= $filename; 189 | 190 | $x = 0; 191 | while (isset($manifestrev[strtolower($path . ($x ? "_" . ($x + 1) : "") . $ops[$key]["ext"])]) || is_dir($path . ($x ? "_" . ($x + 1) : "") . $ops[$key]["ext"])) $x++; 192 | 193 | $path .= ($x ? "_" . ($x + 1) : "") . $ops[$key]["ext"]; 194 | 195 | $opsdata[$key]["path"] = $path; 196 | 197 | // Reserve an entry in the reverse manifest for the full path/filename. 198 | $manifestrev[strtolower($path)] = $path; 199 | 200 | //var_dump($opsdata[$key]["path"]); 201 | //var_dump($manifestrev); 202 | } 203 | 204 | // Maps a manifest item to a static path on disk. 205 | $processedurls = array(); 206 | function MapManifestResourceItem($parenturl, $url) 207 | { 208 | global $manifest, $processedurls, $opsdata; 209 | 210 | // Strip scheme if HTTP/HTTPS. Otherwise, just return the URL as-is (e.g. mailto: and data: URIs). 211 | if (strtolower(substr($url, 0, 7)) === "http://") $url2 = substr($url, 5); 212 | else if (strtolower(substr($url, 0, 8)) === "https://") $url2 = substr($url, 6); 213 | else return $url; 214 | 215 | // If already processed and valid, return the relative reference to the path on disk. 216 | if ($parenturl !== false && isset($opsdata[$parenturl]) && (isset($manifest[$url2]) || isset($opsdata[$url]))) 217 | { 218 | $path = explode("/", $opsdata[$parenturl]["path"]); 219 | $path2 = explode("/", (isset($manifest[$url2]) ? $manifest[$url2] : $opsdata[$url]["path"])); 220 | 221 | array_pop($path); 222 | 223 | while (count($path) && count($path2) && $path[0] === $path2[0]) 224 | { 225 | array_shift($path); 226 | array_shift($path2); 227 | } 228 | 229 | $path2 = str_repeat("../", count($path)) . implode("/", $path2); 230 | 231 | return $path2; 232 | } 233 | 234 | // If already processed but not valid (e.g. a 404 error), just return the URL. 235 | if (isset($processedurls[$url])) return $url; 236 | 237 | return false; 238 | } 239 | 240 | // Generates a leaf node and prevents the parent from completing until the document URLs are updated. 241 | function PrepareManifestResourceItem($parenturl, $forcedext, $url) 242 | { 243 | global $ops, $helper; 244 | 245 | $pos = strpos($url, "#"); 246 | if ($pos === false) $fragment = false; 247 | else 248 | { 249 | $fragment = substr($url, $pos); 250 | $url = substr($url, 0, $pos); 251 | } 252 | 253 | // Skip downloading if the item has already been processed. 254 | $url2 = MapManifestResourceItem($parenturl, $url); 255 | if ($url2 !== false) return $url2 . $fragment; 256 | 257 | // Queue the resource request. 258 | $key = $url; 259 | 260 | if (!isset($ops[$key])) 261 | { 262 | $ops[$key] = array( 263 | "type" => "res", 264 | "status" => "download", 265 | "depth" => 0, 266 | "retries" => 3, 267 | "ext" => $forcedext, 268 | "waiting" => array(), 269 | "web" => ($parenturl === false ? new WebBrowser(array("followlocation" => false)) : clone $ops[$parenturl]["web"]), 270 | "options" => array( 271 | "pre_retrievewebpage_callback" => "DisplayURL" 272 | ) 273 | ); 274 | 275 | $ops[$key]["web"]->ProcessAsync($helper, $key, NULL, $url, $ops[$key]["options"]); 276 | } 277 | 278 | // Set the waiting status for the parent. 279 | if ($parenturl !== false) 280 | { 281 | if ($ops[$parenturl]["status"] === "waiting") $ops[$parenturl]["wait_refs"]++; 282 | else 283 | { 284 | $ops[$parenturl]["status"] = "waiting"; 285 | $ops[$parenturl]["wait_refs"] = 1; 286 | } 287 | 288 | $ops[$key]["waiting"][] = $parenturl; 289 | } 290 | 291 | return $url; 292 | } 293 | 294 | // Locate additional files to import in CSS. Doesn't implement a state engine. 295 | function ProcessCSS($css, $parenturl, $baseurl) 296 | { 297 | $result = $css; 298 | 299 | // Strip comments. 300 | $css = str_replace("<" . "!--", " ", $css); 301 | $css = str_replace("--" . ">", " ", $css); 302 | while (($pos = strpos($css, "/*")) !== false) 303 | { 304 | $pos2 = strpos($css, "*/", $pos + 2); 305 | if ($pos2 === false) $pos2 = strlen($css); 306 | else $pos2 += 2; 307 | 308 | $css = substr($css, 0, $pos) . substr($css, $pos2); 309 | } 310 | 311 | // Alter @import lines. 312 | $pos = 0; 313 | while (($pos = stripos($css, "@import", $pos)) !== false) 314 | { 315 | $semipos = strpos($css, ";", $pos); 316 | if ($semipos === false) break; 317 | 318 | $pos2 = strpos($css, "'", $pos); 319 | if ($pos2 === false) $pos2 = strpos($css, "\"", $pos); 320 | if ($pos2 === false) break; 321 | 322 | $pos3 = strpos($css, $css[$pos2], $pos2 + 1); 323 | if ($pos3 === false) break; 324 | 325 | if ($pos2 < $semipos && $pos3 < $semipos) 326 | { 327 | $url = HTTP::ConvertRelativeToAbsoluteURL($baseurl, substr($css, $pos2 + 1, $pos3 - $pos2 - 1)); 328 | 329 | $result = str_replace(substr($css, $pos2, $pos3 - $pos2 + 1), $css[$pos2] . PrepareManifestResourceItem($parenturl, ".css", $url) . $css[$pos2], $result); 330 | } 331 | 332 | $pos = $semipos + 1; 333 | } 334 | 335 | // Alter url() values. 336 | $pos = 0; 337 | while (($pos = stripos($css, "url(", $pos)) !== false) 338 | { 339 | $endpos = strpos($css, ")", $pos); 340 | if ($endpos === false) break; 341 | 342 | $pos2 = strpos($css, "'", $pos); 343 | if ($pos2 !== false && $pos2 > $endpos) $pos2 = false; 344 | if ($pos2 === false) $pos2 = strpos($css, "\"", $pos); 345 | 346 | if ($pos2 === false || $pos2 > $endpos) 347 | { 348 | $pos2 = $pos + 3; 349 | $pos3 = $endpos; 350 | } 351 | else 352 | { 353 | $pos3 = strpos($css, $css[$pos2], $pos2 + 1); 354 | if ($pos3 === false || $pos3 > $endpos) $pos3 = $endpos; 355 | } 356 | 357 | $url = HTTP::ConvertRelativeToAbsoluteURL($baseurl, substr($css, $pos2 + 1, $pos3 - $pos2 - 1)); 358 | 359 | $result = str_replace(substr($css, $pos2, $pos3 - $pos2 + 1), $css[$pos2] . PrepareManifestResourceItem($parenturl, false, $url) . $css[$pos3], $result); 360 | 361 | $pos = $endpos + 1; 362 | } 363 | 364 | return $result; 365 | } 366 | 367 | function ProcessContent($key, $final) 368 | { 369 | global $ops, $opsdata, $htmloptions, $initurl2, $linkdepth, $helper; 370 | 371 | // Process HTML, altering URLs as necessary. 372 | if ($ops[$key]["type"] === "node" && $ops[$key]["ext"] === ".html") 373 | { 374 | $html = TagFilter::Explode($opsdata[$key]["content"], $htmloptions); 375 | $root = $html->Get(); 376 | 377 | $urlinfo = HTTP::ExtractURL($opsdata[$key]["url"]); 378 | 379 | // Handle images. 380 | $rows = $root->Find('img[src],img[srcset],picture source[srcset]'); 381 | foreach ($rows as $row) 382 | { 383 | if (isset($row->src)) 384 | { 385 | $url = HTTP::ConvertRelativeToAbsoluteURL($urlinfo, $row->src); 386 | 387 | $row->src = PrepareManifestResourceItem($key, false, $url); 388 | } 389 | 390 | if (isset($row->srcset)) 391 | { 392 | $urls = explode(",", $row->srcset); 393 | $urls2 = array(); 394 | foreach ($urls as $url) 395 | { 396 | $url = trim($url); 397 | $pos = strrpos($url, " "); 398 | if ($pos !== false) 399 | { 400 | $url2 = HTTP::ConvertRelativeToAbsoluteURL($urlinfo, trim(substr($url, 0, $pos))); 401 | $size = substr($url, $pos + 1); 402 | 403 | $urls2[] = PrepareManifestResourceItem($key, false, $url2) . " " . $size; 404 | } 405 | } 406 | 407 | $row->srcset = implode(", ", $urls2); 408 | } 409 | } 410 | 411 | // Handle link tags with hrefs. 412 | $rows = $root->Find('link[href],use[xlink\:href]'); 413 | foreach ($rows as $row) 414 | { 415 | $url = HTTP::ConvertRelativeToAbsoluteURL($urlinfo, (isset($row->href) ? $row->href : $row->{"xlink:href"})); 416 | 417 | $row->href = PrepareManifestResourceItem($key, ((isset($row->rel) && strtolower($row->rel) === "stylesheet") || (isset($row->type) && strtolower($row->type) === "text/css") ? ".css" : false), $url); 418 | } 419 | 420 | // Handle external Javascript. 421 | $rows = $root->Find('script[src]'); 422 | foreach ($rows as $row) 423 | { 424 | $url = HTTP::ConvertRelativeToAbsoluteURL($urlinfo, $row->src); 425 | 426 | $row->src = PrepareManifestResourceItem($key, ".js", $url); 427 | } 428 | 429 | // Handle style tags. 430 | $rows = $root->Find('style'); 431 | foreach ($rows as $row) 432 | { 433 | $children = $row->Children(true); 434 | foreach ($children as $child) 435 | { 436 | if ($child->Type() === "content") 437 | { 438 | $child->Text(ProcessCSS($child->Text(), $key, $urlinfo)); 439 | } 440 | } 441 | } 442 | 443 | // Handle inline styles. 444 | $rows = $root->Find('[style]'); 445 | foreach ($rows as $row) 446 | { 447 | $row->style = ProcessCSS($row->style, $key, $urlinfo); 448 | } 449 | 450 | // Handle anchor tags and iframes. 451 | $rows = $root->Find('a[href],iframe[src]'); 452 | foreach ($rows as $row) 453 | { 454 | $url = ($row->Tag() === "iframe" ? $row->src : $row->href); 455 | 456 | // Skip altering fragment-only URIs. The browser knows how to natively handle these. 457 | if (substr($url, 0, 1) === "#") continue; 458 | 459 | $url = HTTP::ConvertRelativeToAbsoluteURL($urlinfo, $url); 460 | $url2 = HTTP::ExtractURL($url); 461 | 462 | // Only follow links on the same domain. 463 | if (strcasecmp($url2["authority"], $initurl2["authority"]) == 0 && ($url2["scheme"] === "http" || $url2["scheme"] === "https")) 464 | { 465 | if ($url2["path"] === "") 466 | { 467 | $url2["path"] = "/"; 468 | $url = HTTP::CondenseURL($url2); 469 | } 470 | 471 | $pos = strpos($url, "#"); 472 | if ($pos === false) $fragment = false; 473 | else 474 | { 475 | $fragment = substr($url, $pos); 476 | $url = substr($url, 0, $pos); 477 | } 478 | 479 | $url2 = MapManifestResourceItem($key, $url); 480 | if ($url2 !== false) 481 | { 482 | if ($row->Tag() === "iframe") $row->src = $url2 . $fragment; 483 | else $row->href = $url2 . $fragment; 484 | } 485 | else 486 | { 487 | if ($row->Tag() === "iframe") $row->src = $url . $fragment; 488 | else $row->href = $url . $fragment; 489 | 490 | if ($linkdepth === false || $ops[$key]["depth"] < $linkdepth) 491 | { 492 | // Queue up another node. 493 | $key2 = $url; 494 | 495 | if (!isset($ops[$key2])) 496 | { 497 | $ops[$key2] = array( 498 | "type" => "node", 499 | "status" => "download", 500 | "depth" => $ops[$key]["depth"] + 1, 501 | "retries" => 3, 502 | "ext" => false, 503 | "waiting" => array(), 504 | "web" => clone $ops[$key]["web"], 505 | "options" => array( 506 | "pre_retrievewebpage_callback" => "DisplayURL" 507 | ) 508 | ); 509 | 510 | $ops[$key]["web"]->ProcessAsync($helper, $key2, NULL, $url, $ops[$key2]["options"]); 511 | } 512 | 513 | if ($key !== $key2) 514 | { 515 | if ($ops[$key]["status"] === "waiting") $ops[$key]["wait_refs"]++; 516 | else 517 | { 518 | $ops[$key]["status"] = "waiting"; 519 | $ops[$key]["wait_refs"] = 1; 520 | } 521 | 522 | $ops[$key2]["waiting"][] = $key; 523 | } 524 | } 525 | } 526 | } 527 | } 528 | 529 | // Mix down the content back into HTML. 530 | if ($final) $opsdata[$key]["content"] = $root->GetOuterHTML(); 531 | } 532 | 533 | // Process CSS, altering URLs as necessary. 534 | if ($ops[$key]["ext"] === ".css") 535 | { 536 | $urlinfo = HTTP::ExtractURL($opsdata[$key]["url"]); 537 | 538 | $result = ProcessCSS($opsdata[$key]["content"], $key, $urlinfo); 539 | 540 | if ($final) $opsdata[$key]["content"] = $result; 541 | } 542 | } 543 | 544 | function SaveQueues() 545 | { 546 | global $ops, $opsfile, $destpath, $manifest, $manifestfile; 547 | 548 | file_put_contents($manifestfile, json_encode($manifest, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); 549 | 550 | $ops2 = array(); 551 | foreach ($ops as $url => $info) 552 | { 553 | $info["web_state"] = $info["web"]->GetState(); 554 | unset($info["web"]); 555 | 556 | $ops2[$url] = $info; 557 | } 558 | 559 | file_put_contents($opsfile, json_encode($ops2, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); 560 | } 561 | 562 | // Load the URL mapping manifest and operations files if they exist in order to continue wherever this script left off. 563 | $manifest = @json_decode(file_get_contents($manifestfile), true); 564 | if (!is_array($manifest)) $manifest = array(); 565 | 566 | $manifestrev = array(); 567 | foreach ($manifest as $key => $val) 568 | { 569 | $vals = explode("/", $val); 570 | $val = array_shift($vals) . "/"; 571 | while (count($vals)) 572 | { 573 | $val .= array_shift($vals); 574 | 575 | $manifestrev[strtolower($val)] = $val; 576 | 577 | $val .= "/"; 578 | } 579 | } 580 | 581 | $ops = @json_decode(file_get_contents($opsfile), true); 582 | if (is_array($ops)) 583 | { 584 | // Initialize the operations queue. 585 | foreach ($ops as $url => &$info) 586 | { 587 | $key = $url; 588 | 589 | $info["status"] = "download"; 590 | $info["retries"] = 3; 591 | $info["web"] = new WebBrowser($info["web_state"]); 592 | $info["web"]->ProcessAsync($helper, $key, NULL, $url, $info["options"]); 593 | 594 | unset($info["web_state"]); 595 | } 596 | 597 | unset($info); 598 | } 599 | else 600 | { 601 | // Queue the first operation. 602 | $ops = array(); 603 | 604 | $key = $initurl; 605 | 606 | $ops[$key] = array( 607 | "type" => "node", 608 | "status" => "download", 609 | "depth" => 0, 610 | "retries" => 3, 611 | "ext" => false, 612 | "waiting" => array(), 613 | "web" => new WebBrowser(), 614 | "options" => array( 615 | "pre_retrievewebpage_callback" => "DisplayURL" 616 | ) 617 | ); 618 | 619 | $ops[$key]["web"]->ProcessAsync($helper, $key, NULL, $initurl, $ops[$key]["options"]); 620 | 621 | // Queue 'favicon.ico'. 622 | // PrepareManifestResourceItem(false, ".ico", HTTP::ConvertRelativeToAbsoluteURL($initurl, "/favicon.ico")); 623 | 624 | // Queue 'robots.txt'. 625 | // PrepareManifestResourceItem(false, ".txt", HTTP::ConvertRelativeToAbsoluteURL($initurl, "/robots.txt")); 626 | 627 | SaveQueues(); 628 | } 629 | 630 | $opsdata = array(); 631 | 632 | // Run the main loop. 633 | $result = $helper->Wait(); 634 | while ($result["success"]) 635 | { 636 | // Process finished items. 637 | foreach ($result["removed"] as $key => $info) 638 | { 639 | if (!$info["result"]["success"]) 640 | { 641 | $ops[$key]["retries"]--; 642 | if ($ops[$key]["retries"]) $ops[$key]["web"]->ProcessAsync($helper, $key, NULL, $key, $info["tempoptions"]); 643 | 644 | echo "Error retrieving URL (" . $key . "). " . ($ops[$key]["retries"] > 0 ? "Retrying in a moment. " : "") . $info["result"]["error"] . " (" . $info["result"]["errorcode"] . ")\n"; 645 | } 646 | else 647 | { 648 | echo "[" . number_format(count($ops), 0) . " ops] Processing '" . $key . "'.\n"; 649 | 650 | // Just report non-200 OK responses. Store the data except for 404 errors. 651 | if ($info["result"]["response"]["code"] != 200) echo "Error retrieving URL '" . $info["result"]["url"] . "'.\nServer returned: " . $info["result"]["response"]["line"] . "\n"; 652 | 653 | $opsdata[$key] = array( 654 | "httpcode" => $info["result"]["response"]["code"], 655 | "url" => $info["result"]["url"], 656 | "content" => $info["result"]["body"] 657 | ); 658 | 659 | unset($info["result"]["body"]); 660 | 661 | // Get the final file extension to use. 662 | if ($ops[$key]["ext"] === false) $ops[$key]["ext"] = GetResultFileExtension($info["result"]); 663 | 664 | // Calculate the reverse manifest path. 665 | SetReverseManifestPath($key); 666 | 667 | // Process the incoming content, if relevant. 668 | ProcessContent($key, false); 669 | 670 | // Walk parents and reduce the number of resources being waited on. 671 | $process = array(); 672 | if ($ops[$key]["status"] !== "waiting") 673 | { 674 | $process[] = $key; 675 | 676 | // Process the content a second time. This time updating all valid, processed URLs with static URLs. 677 | ProcessContent($key, true); 678 | } 679 | 680 | foreach ($ops[$key]["waiting"] as $pkey) 681 | { 682 | $ops[$pkey]["wait_refs"]--; 683 | 684 | if ($ops[$pkey]["wait_refs"] <= 0) 685 | { 686 | $process[] = $pkey; 687 | 688 | // Process the content a second time. This time updating all valid, processed URLs with static URLs. 689 | ProcessContent($pkey, true); 690 | } 691 | } 692 | 693 | $ops[$key]["waiting"] = array(); 694 | 695 | // Store ready documents to disk. 696 | while (count($process)) 697 | { 698 | $key2 = array_shift($process); 699 | 700 | if ($opsdata[$key2]["httpcode"] >= 400) echo "[" . number_format(count($ops), 0) . " ops] Finalizing '" . $key2 . "'.\n"; 701 | else 702 | { 703 | echo "[" . number_format(count($ops), 0) . " ops] Saving '" . $key2 . "' to '" . $destpath . $opsdata[$key2]["path"] . "'.\n"; 704 | 705 | $manifest[str_replace(array("http://", "https://"), "//", $key2)] = $opsdata[$key2]["path"]; 706 | 707 | // Write data to disk. 708 | file_put_contents($destpath . $opsdata[$key2]["path"], $opsdata[$key2]["content"]); 709 | } 710 | 711 | $processedurls[$key2] = true; 712 | 713 | unset($opsdata[$key2]); 714 | 715 | // Walk parents and reduce the number of resources being waited on. 716 | foreach ($ops[$key2]["waiting"] as $pkey) 717 | { 718 | $ops[$pkey]["wait_refs"]--; 719 | 720 | if ($ops[$pkey]["wait_refs"] <= 0) 721 | { 722 | $process[] = $pkey; 723 | 724 | // Process the content a second time. This time updating all valid, processed URLs with static URLs. 725 | ProcessContent($pkey, true); 726 | } 727 | } 728 | 729 | unset($ops[$key2]); 730 | } 731 | } 732 | } 733 | 734 | if (count($result["removed"])) SaveQueues(); 735 | 736 | // Break out of the loop when there is nothing left to do. 737 | if (!$helper->NumObjects()) break; 738 | 739 | $result = $helper->Wait(); 740 | } 741 | 742 | // Final message. 743 | if (count($ops)) 744 | { 745 | echo "Unable to process the following URLs:\n\n"; 746 | 747 | foreach ($ops as $url => $info) 748 | { 749 | echo " " . $url . "\n"; 750 | } 751 | 752 | echo "\n"; 753 | echo "Done, with errors.\n"; 754 | } 755 | else 756 | { 757 | echo "Done.\n"; 758 | } 759 | ?> -------------------------------------------------------------------------------- /support/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /support/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/multi_async_helper.php: -------------------------------------------------------------------------------- 1 | objs = array(); 12 | $this->queuedobjs = array(); 13 | $this->limit = false; 14 | } 15 | 16 | public function SetConcurrencyLimit($limit) 17 | { 18 | $this->limit = $limit; 19 | } 20 | 21 | public function Set($key, $obj, $callback) 22 | { 23 | if (is_callable($callback)) 24 | { 25 | $this->queuedobjs[$key] = array( 26 | "obj" => $obj, 27 | "callback" => $callback 28 | ); 29 | 30 | unset($this->objs[$key]); 31 | } 32 | } 33 | 34 | public function NumObjects() 35 | { 36 | return count($this->queuedobjs) + count($this->objs); 37 | } 38 | 39 | public function GetObject($key) 40 | { 41 | if (isset($this->queuedobjs[$key])) $result = $this->queuedobjs[$key]["obj"]; 42 | else if (isset($this->objs[$key])) $result = $this->objs[$key]["obj"]; 43 | else $result = false; 44 | 45 | return $result; 46 | } 47 | 48 | // To be able to change a callback on the fly. 49 | public function SetCallback($key, $callback) 50 | { 51 | if (is_callable($callback)) 52 | { 53 | if (isset($this->queuedobjs[$key])) $this->queuedobjs[$key]["callback"] = $callback; 54 | else if (isset($this->objs[$key])) $this->objs[$key]["callback"] = $callback; 55 | } 56 | } 57 | 58 | private function InternalDetach($key, $cleanup) 59 | { 60 | if (isset($this->queuedobjs[$key])) 61 | { 62 | call_user_func_array($this->queuedobjs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->queuedobjs[$key]["obj"])); 63 | $result = $this->queuedobjs[$key]["obj"]; 64 | unset($this->queuedobjs[$key]); 65 | } 66 | else if (isset($this->objs[$key])) 67 | { 68 | call_user_func_array($this->objs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->objs[$key]["obj"])); 69 | $result = $this->objs[$key]["obj"]; 70 | unset($this->objs[$key]); 71 | } 72 | else 73 | { 74 | $result = false; 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | public function Detach($key) 81 | { 82 | return $this->InternalDetach($key, false); 83 | } 84 | 85 | public function Remove($key) 86 | { 87 | return $this->InternalDetach($key, true); 88 | } 89 | 90 | // A few default functions for direct file/socket handles. 91 | public static function ReadOnly($mode, &$data, $key, $fp) 92 | { 93 | switch ($mode) 94 | { 95 | case "init": 96 | case "update": 97 | { 98 | // Move to/Keep in the live queue. 99 | if (is_resource($fp)) $data = true; 100 | 101 | break; 102 | } 103 | case "read": 104 | case "write": 105 | case "writefps": 106 | { 107 | break; 108 | } 109 | case "readfps": 110 | { 111 | $data[$key] = $fp; 112 | 113 | break; 114 | } 115 | case "cleanup": 116 | { 117 | if ($data === true) @fclose($fp); 118 | 119 | break; 120 | } 121 | } 122 | } 123 | 124 | public static function WriteOnly($mode, &$data, $key, $fp) 125 | { 126 | switch ($mode) 127 | { 128 | case "init": 129 | case "update": 130 | { 131 | // Move to/Keep in the live queue. 132 | if (is_resource($fp)) $data = true; 133 | 134 | break; 135 | } 136 | case "read": 137 | case "readfps": 138 | case "write": 139 | { 140 | break; 141 | } 142 | case "writefps": 143 | { 144 | $data[$key] = $fp; 145 | 146 | break; 147 | } 148 | case "cleanup": 149 | { 150 | if ($data === true) @fclose($fp); 151 | 152 | break; 153 | } 154 | } 155 | } 156 | 157 | public static function ReadAndWrite($mode, &$data, $key, $fp) 158 | { 159 | switch ($mode) 160 | { 161 | case "init": 162 | case "update": 163 | { 164 | // Move to/Keep in the live queue. 165 | if (is_resource($fp)) $data = true; 166 | 167 | break; 168 | } 169 | case "read": 170 | case "write": 171 | { 172 | break; 173 | } 174 | case "readfps": 175 | case "writefps": 176 | { 177 | $data[$key] = $fp; 178 | 179 | break; 180 | } 181 | case "cleanup": 182 | { 183 | if ($data === true) @fclose($fp); 184 | 185 | break; 186 | } 187 | } 188 | } 189 | 190 | public function Wait($timeout = false) 191 | { 192 | // Move queued objects to live. 193 | $result2 = array("success" => true, "read" => array(), "write" => array(), "removed" => array(), "new" => array()); 194 | while (count($this->queuedobjs) && ($this->limit === false || count($this->objs) < $this->limit)) 195 | { 196 | $info = reset($this->queuedobjs); 197 | $key = key($this->queuedobjs); 198 | unset($this->queuedobjs[$key]); 199 | 200 | $result2["new"][$key] = $key; 201 | 202 | $keep = false; 203 | call_user_func_array($info["callback"], array("init", &$keep, $key, &$info["obj"])); 204 | 205 | $this->objs[$key] = $info; 206 | 207 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 208 | } 209 | 210 | // Walk the objects looking for read and write handles. 211 | $readfps = array(); 212 | $writefps = array(); 213 | $exceptfps = NULL; 214 | foreach ($this->objs as $key => &$info) 215 | { 216 | $keep = false; 217 | call_user_func_array($info["callback"], array("update", &$keep, $key, &$info["obj"])); 218 | 219 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 220 | else 221 | { 222 | call_user_func_array($info["callback"], array("readfps", &$readfps, $key, &$info["obj"])); 223 | call_user_func_array($info["callback"], array("writefps", &$writefps, $key, &$info["obj"])); 224 | } 225 | } 226 | if (!count($readfps)) $readfps = NULL; 227 | if (!count($writefps)) $writefps = NULL; 228 | 229 | // Wait for something to happen. 230 | if (isset($readfps) || isset($writefps)) 231 | { 232 | if ($timeout === false) $timeout = NULL; 233 | $readfps2 = $readfps; 234 | $writefps2 = $writefps; 235 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 236 | if ($result === false) return array("success" => false, "error" => self::MAHTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 237 | else if ($result > 0) 238 | { 239 | if (isset($readfps)) 240 | { 241 | $readfps3 = array(); 242 | foreach ($readfps as $key => $fp) 243 | { 244 | if (!isset($readfps2[$key]) || $readfps2[$key] !== $fp) 245 | { 246 | foreach ($readfps2 as $key2 => $fp2) 247 | { 248 | if ($fp === $fp2) $key = $key2; 249 | } 250 | } 251 | 252 | if (isset($this->objs[$key])) 253 | { 254 | call_user_func_array($this->objs[$key]["callback"], array("read", &$fp, $key, &$this->objs[$key]["obj"])); 255 | 256 | $readfps3[$key] = $fp; 257 | } 258 | } 259 | 260 | $result2["read"] = $readfps3; 261 | } 262 | 263 | if (isset($writefps)) 264 | { 265 | $writefps3 = array(); 266 | foreach ($writefps as $key => $fp) 267 | { 268 | if (!isset($writefps2[$key]) || $writefps2[$key] !== $fp) 269 | { 270 | foreach ($writefps2 as $key2 => $fp2) 271 | { 272 | if ($fp === $fp2) $key = $key2; 273 | } 274 | } 275 | 276 | if (isset($this->objs[$key])) 277 | { 278 | call_user_func_array($this->objs[$key]["callback"], array("write", &$fp, $key, &$this->objs[$key]["obj"])); 279 | 280 | $readfps3[$key] = $fp; 281 | } 282 | } 283 | 284 | $result2["write"] = $writefps3; 285 | } 286 | } 287 | } 288 | 289 | $result2["numleft"] = count($this->queuedobjs) + count($this->objs); 290 | 291 | return $result2; 292 | } 293 | 294 | public static function MAHTranslate() 295 | { 296 | $args = func_get_args(); 297 | if (!count($args)) return ""; 298 | 299 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 300 | } 301 | } 302 | ?> -------------------------------------------------------------------------------- /support/utf_utils.php: -------------------------------------------------------------------------------- 1 | = 0x0300 && $val <= 0x036F) || ($val >= 0x1DC0 && $val <= 0x1DFF) || ($val >= 0x20D0 && $val <= 0x20FF) || ($val >= 0xFE20 && $val <= 0xFE2F)); 21 | } 22 | 23 | public static function Convert($data, $srctype, $desttype) 24 | { 25 | $arr = is_array($data); 26 | if ($arr) $srctype = self::UTF32_ARRAY; 27 | $x = 0; 28 | $y = ($arr ? count($data) : strlen($data)); 29 | $result = ($desttype === self::UTF32_ARRAY ? array() : ""); 30 | if (!$arr && $srctype === self::UTF32_ARRAY) return $result; 31 | 32 | $first = true; 33 | 34 | if ($srctype === self::UTF8_BOM) 35 | { 36 | if (substr($data, 0, 3) === "\xEF\xBB\xBF") $x = 3; 37 | 38 | $srctype = self::UTF8; 39 | } 40 | 41 | if ($srctype === self::UTF16_BOM) 42 | { 43 | if (substr($data, 0, 2) === "\xFE\xFF") 44 | { 45 | $srctype = self::UTF16_BE; 46 | $x = 2; 47 | } 48 | else if (substr($data, 0, 2) === "\xFF\xFE") 49 | { 50 | $srctype = self::UTF16_LE; 51 | $x = 2; 52 | } 53 | else 54 | { 55 | $srctype = self::UTF16_LE; 56 | } 57 | } 58 | 59 | if ($srctype === self::UTF32_BOM) 60 | { 61 | if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") 62 | { 63 | $srctype = self::UTF32_BE; 64 | $x = 4; 65 | } 66 | else if (substr($data, 0, 4) === "\xFF\xFE\x00\x00") 67 | { 68 | $srctype = self::UTF32_LE; 69 | $x = 4; 70 | } 71 | else 72 | { 73 | $srctype = self::UTF32_LE; 74 | } 75 | } 76 | 77 | while ($x < $y) 78 | { 79 | // Read the next valid code point. 80 | $val = false; 81 | 82 | switch ($srctype) 83 | { 84 | case self::UTF8: 85 | { 86 | $tempchr = ord($data[$x]); 87 | if ($tempchr <= 0x7F) 88 | { 89 | $val = $tempchr; 90 | $x++; 91 | } 92 | else if ($tempchr < 0xC2) $x++; 93 | else 94 | { 95 | $left = $y - $x; 96 | if ($left < 2) $x++; 97 | else 98 | { 99 | $tempchr2 = ord($data[$x + 1]); 100 | 101 | if (($tempchr >= 0xC2 && $tempchr <= 0xDF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) 102 | { 103 | $val = (($tempchr & 0x1F) << 6) | ($tempchr2 & 0x3F); 104 | $x += 2; 105 | } 106 | else if ($left < 3) $x++; 107 | else 108 | { 109 | $tempchr3 = ord($data[$x + 2]); 110 | 111 | if ($tempchr3 < 0x80 || $tempchr3 > 0xBF) $x++; 112 | else 113 | { 114 | if (($tempchr == 0xE0 && ($tempchr2 >= 0xA0 && $tempchr2 <= 0xBF)) || ((($tempchr >= 0xE1 && $tempchr <= 0xEC) || $tempchr == 0xEE || $tempchr == 0xEF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xED && ($tempchr2 >= 0x80 && $tempchr2 <= 0x9F))) 115 | { 116 | $val = (($tempchr & 0x0F) << 12) | (($tempchr2 & 0x3F) << 6) | ($tempchr3 & 0x3F); 117 | $x += 3; 118 | } 119 | else if ($left < 4) $x++; 120 | else 121 | { 122 | $tempchr4 = ord($data[$x + 3]); 123 | 124 | if ($tempchr4 < 0x80 || $tempchr4 > 0xBF) $x++; 125 | else if (($tempchr == 0xF0 && ($tempchr2 >= 0x90 && $tempchr2 <= 0xBF)) || (($tempchr >= 0xF1 && $tempchr <= 0xF3) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xF4 && ($tempchr2 >= 0x80 && $tempchr2 <= 0x8F))) 126 | { 127 | $val = (($tempchr & 0x07) << 18) | (($tempchr2 & 0x3F) << 12) | (($tempchr3 & 0x3F) << 6) | ($tempchr4 & 0x3F); 128 | $x += 4; 129 | } 130 | else 131 | { 132 | $x++; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | break; 141 | } 142 | case self::UTF16_LE: 143 | { 144 | if ($x + 1 >= $y) $x = $y; 145 | else 146 | { 147 | $val = unpack("v", substr($data, $x, 2))[1]; 148 | $x += 2; 149 | 150 | if ($val >= 0xD800 && $val <= 0xDBFF) 151 | { 152 | if ($x + 1 >= $y) 153 | { 154 | $x = $y; 155 | $val = false; 156 | } 157 | else 158 | { 159 | $val2 = unpack("v", substr($data, $x, 2))[1]; 160 | 161 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 162 | else 163 | { 164 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 165 | $x += 2; 166 | } 167 | } 168 | } 169 | } 170 | 171 | break; 172 | } 173 | case self::UTF16_BE: 174 | { 175 | if ($x + 1 >= $y) $x = $y; 176 | else 177 | { 178 | $val = unpack("n", substr($data, $x, 2))[1]; 179 | $x += 2; 180 | 181 | if ($val >= 0xD800 && $val <= 0xDBFF) 182 | { 183 | if ($x + 1 >= $y) 184 | { 185 | $x = $y; 186 | $val = false; 187 | } 188 | else 189 | { 190 | $val2 = unpack("n", substr($data, $x, 2))[1]; 191 | 192 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 193 | else 194 | { 195 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 196 | $x += 2; 197 | } 198 | } 199 | } 200 | } 201 | 202 | break; 203 | } 204 | case self::UTF32_LE: 205 | { 206 | if ($x + 3 >= $y) $x = $y; 207 | else 208 | { 209 | $val = unpack("V", substr($data, $x, 4))[1]; 210 | $x += 4; 211 | } 212 | 213 | break; 214 | } 215 | case self::UTF32_BE: 216 | { 217 | if ($x + 3 >= $y) $x = $y; 218 | else 219 | { 220 | $val = unpack("N", substr($data, $x, 4))[1]; 221 | $x += 4; 222 | } 223 | 224 | break; 225 | } 226 | case self::UTF32_ARRAY: 227 | { 228 | $val = (int)$data[$x]; 229 | $x++; 230 | 231 | break; 232 | } 233 | default: $x = $y; break; 234 | } 235 | 236 | // Make sure it is a valid Unicode value. 237 | // 0xD800-0xDFFF are for UTF-16 surrogate pairs. Invalid characters. 238 | // 0xFDD0-0xFDEF are non-characters. 239 | // 0x*FFFE and 0x*FFFF are reserved. 240 | // The largest possible character is 0x10FFFF. 241 | // First character can't be a combining code point. 242 | if ($val !== false && !($val < 0 || ($val >= 0xD800 && $val <= 0xDFFF) || ($val >= 0xFDD0 && $val <= 0xFDEF) || ($val & 0xFFFE) == 0xFFFE || $val > 0x10FFFF || ($first && self::IsCombiningCodePoint($val)))) 243 | { 244 | if ($first) 245 | { 246 | if ($desttype === self::UTF8_BOM) 247 | { 248 | $result .= "\xEF\xBB\xBF"; 249 | 250 | $desttype = self::UTF8; 251 | } 252 | 253 | if ($desttype === self::UTF16_BOM) 254 | { 255 | $result .= "\xFF\xFE"; 256 | 257 | $desttype = self::UTF16_LE; 258 | } 259 | 260 | if ($srctype === self::UTF32_BOM) 261 | { 262 | $result .= "\xFF\xFE\x00\x00"; 263 | 264 | $desttype = self::UTF32_LE; 265 | } 266 | 267 | $first = false; 268 | } 269 | 270 | switch ($desttype) 271 | { 272 | case self::UTF8: 273 | { 274 | if ($val <= 0x7F) $result .= chr($val); 275 | else if ($val <= 0x7FF) $result .= chr(0xC0 | ($val >> 6)) . chr(0x80 | ($val & 0x3F)); 276 | else if ($val <= 0xFFFF) $result .= chr(0xE0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 277 | else if ($val <= 0x10FFFF) $result .= chr(0xF0 | ($val >> 18)) . chr(0x80 | (($val >> 12) & 0x3F)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 278 | 279 | break; 280 | } 281 | case self::UTF16_LE: 282 | { 283 | if ($val <= 0xFFFF) $result .= pack("v", $val); 284 | else 285 | { 286 | $val -= 0x10000; 287 | $result .= pack("v", ((($val >> 10) & 0x3FF) + 0xD800)); 288 | $result .= pack("v", (($val & 0x3FF) + 0xDC00)); 289 | } 290 | 291 | break; 292 | } 293 | case self::UTF16_BE: 294 | { 295 | if ($val <= 0xFFFF) $result .= pack("n", $val); 296 | else 297 | { 298 | $val -= 0x10000; 299 | $result .= pack("n", ((($val >> 10) & 0x3FF) + 0xD800)); 300 | $result .= pack("n", (($val & 0x3FF) + 0xDC00)); 301 | } 302 | 303 | break; 304 | } 305 | case self::UTF32_LE: 306 | { 307 | $result .= pack("V", $val); 308 | 309 | break; 310 | } 311 | case self::UTF32_BE: 312 | { 313 | $result .= pack("N", $val); 314 | 315 | break; 316 | } 317 | case self::UTF32_ARRAY: 318 | { 319 | $result[] = $val; 320 | 321 | break; 322 | } 323 | default: $x = $y; break; 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | 332 | protected const PUNYCODE_BASE = 36; 333 | protected const PUNYCODE_TMIN = 1; 334 | protected const PUNYCODE_TMAX = 26; 335 | protected const PUNYCODE_SKEW = 38; 336 | protected const PUNYCODE_DAMP = 700; 337 | protected const PUNYCODE_INITIAL_BIAS = 72; 338 | protected const PUNYCODE_INITIAL_N = 0x80; 339 | protected const PUNYCODE_DIGIT_MAP = "abcdefghijklmnopqrstuvwxyz0123456789"; 340 | 341 | public static function ConvertToPunycode($domain) 342 | { 343 | // Reject invalid domain name lengths. 344 | if (strlen($domain) > 255) return false; 345 | 346 | $parts = explode(".", $domain); 347 | 348 | foreach ($parts as $num => $part) 349 | { 350 | // Reject invalid label lengths. 351 | $y = strlen($part); 352 | if ($y > 63) return false; 353 | 354 | // Skip already encoded portions. 355 | if (substr($part, 0, 4) === "xn--") continue; 356 | 357 | // Convert UTF-8 to UTF-32 code points. 358 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 359 | 360 | // Handle ASCII code points. 361 | $part2 = ""; 362 | foreach ($data as $cp) 363 | { 364 | if ($cp <= 0x7F) $part2 .= strtolower(chr($cp)); 365 | } 366 | 367 | $numhandled = strlen($part2); 368 | $y = count($data); 369 | 370 | if ($numhandled >= $y) 371 | { 372 | $parts[$num] = $part2; 373 | 374 | continue; 375 | } 376 | 377 | if ($numhandled) $part2 .= "-"; 378 | 379 | $part2 = "xn--" . $part2; 380 | 381 | if (strlen($part2) > 63) return false; 382 | 383 | $bias = self::PUNYCODE_INITIAL_BIAS; 384 | $n = self::PUNYCODE_INITIAL_N; 385 | $delta = 0; 386 | $first = true; 387 | 388 | while ($numhandled < $y) 389 | { 390 | // Find the next largest unhandled code point. 391 | $cp2 = 0x01000000; 392 | foreach ($data as $cp) 393 | { 394 | if ($cp >= $n && $cp2 > $cp) $cp2 = $cp; 395 | } 396 | 397 | // Increase delta but prevent overflow. 398 | $delta += ($cp2 - $n) * ($numhandled + 1); 399 | if ($delta < 0) return false; 400 | $n = $cp2; 401 | 402 | foreach ($data as $cp) 403 | { 404 | if ($cp < $n) 405 | { 406 | $delta++; 407 | 408 | if ($delta < 0) return false; 409 | } 410 | else if ($cp === $n) 411 | { 412 | // Calculate and encode a variable length integer from the delta. 413 | $q = $delta; 414 | $x = 0; 415 | do 416 | { 417 | $x += self::PUNYCODE_BASE; 418 | 419 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 420 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 421 | else $t = $x - $bias; 422 | 423 | if ($q < $t) break; 424 | 425 | $part2 .= self::PUNYCODE_DIGIT_MAP[$t + (($q - $t) % (self::PUNYCODE_BASE - $t))]; 426 | 427 | $q = (int)(($q - $t) / (self::PUNYCODE_BASE - $t)); 428 | 429 | if (strlen($part2) > 63) return false; 430 | } while (1); 431 | 432 | $part2 .= self::PUNYCODE_DIGIT_MAP[$q]; 433 | if (strlen($part2) > 63) return false; 434 | 435 | // Adapt bias. 436 | $numhandled++; 437 | $bias = self::InternalPunycodeAdapt($delta, $numhandled, $first); 438 | $delta = 0; 439 | $first = false; 440 | } 441 | } 442 | 443 | $delta++; 444 | $n++; 445 | } 446 | 447 | $parts[$num] = $part2; 448 | } 449 | 450 | return implode(".", $parts); 451 | } 452 | 453 | public static function ConvertFromPunycode($domain) 454 | { 455 | // Reject invalid domain name lengths. 456 | if (strlen($domain) > 255) return false; 457 | 458 | $parts = explode(".", $domain); 459 | 460 | foreach ($parts as $num => $part) 461 | { 462 | // Reject invalid label lengths. 463 | $y = strlen($part); 464 | if ($y > 63) return false; 465 | 466 | // Skip unencoded portions. 467 | if (substr($part, 0, 4) !== "xn--") continue; 468 | 469 | $part = substr($part, 4); 470 | 471 | // Convert UTF-8 to UTF-32 code points. 472 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 473 | 474 | // Handle ASCII code points. 475 | $hyphen = ord("-"); 476 | for ($x = count($data); $x && $data[$x - 1] !== $hyphen; $x--); 477 | if (!$x) $data2 = array(); 478 | else 479 | { 480 | $data2 = array_splice($data, 0, $x - 1); 481 | 482 | array_shift($data); 483 | } 484 | 485 | $numhandled = count($data2); 486 | 487 | $bias = self::PUNYCODE_INITIAL_BIAS; 488 | $n = self::PUNYCODE_INITIAL_N; 489 | $delta = 0; 490 | $first = true; 491 | 492 | $pos = 0; 493 | $y = count($data); 494 | while ($pos < $y) 495 | { 496 | // Calculate and decode a delta from the variable length integer. 497 | $olddelta = $delta; 498 | $w = 1; 499 | $x = 0; 500 | do 501 | { 502 | $x += self::PUNYCODE_BASE; 503 | 504 | $cp = $data[$pos]; 505 | $pos++; 506 | 507 | if ($cp >= ord("a") && $cp <= ord("z")) $digit = $cp - ord("a"); 508 | else if ($cp >= ord("A") && $cp <= ord("Z")) $digit = $cp - ord("A"); 509 | else if ($cp >= ord("0") && $cp <= ord("9")) $digit = $cp - ord("0") + 26; 510 | else return false; 511 | 512 | $delta += $digit * $w; 513 | if ($delta < 0) return false; 514 | 515 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 516 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 517 | else $t = $x - $bias; 518 | 519 | if ($digit < $t) break; 520 | 521 | $w *= (self::PUNYCODE_BASE - $t); 522 | if ($w < 0) return false; 523 | } while (1); 524 | 525 | // Adapt bias. 526 | $numhandled++; 527 | $bias = self::InternalPunycodeAdapt($delta - $olddelta, $numhandled, $first); 528 | $first = false; 529 | 530 | // Delta was supposed to wrap around from $numhandled to 0, incrementing $n each time, so fix that now. 531 | $n += (int)($delta / $numhandled); 532 | $delta %= $numhandled; 533 | 534 | // Insert $n (the code point) at the delta position. 535 | array_splice($data2, $delta, 0, array($n)); 536 | $delta++; 537 | } 538 | 539 | $parts[$num] = self::Convert($data2, self::UTF32_ARRAY, self::UTF8); 540 | } 541 | 542 | return implode(".", $parts); 543 | } 544 | 545 | // RFC3492 adapt() function. 546 | protected static function InternalPunycodeAdapt($delta, $numpoints, $first) 547 | { 548 | $delta = ($first ? (int)($delta / self::PUNYCODE_DAMP) : $delta >> 1); 549 | $delta += (int)($delta / $numpoints); 550 | 551 | $y = self::PUNYCODE_BASE - self::PUNYCODE_TMIN; 552 | 553 | $condval = (int)(($y * self::PUNYCODE_TMAX) / 2); 554 | for ($x = 0; $delta > $condval; $x += self::PUNYCODE_BASE) $delta = (int)($delta / $y); 555 | 556 | return (int)($x + ((($y + 1) * $delta) / ($delta + self::PUNYCODE_SKEW))); 557 | } 558 | } 559 | ?> -------------------------------------------------------------------------------- /support/websocket.php: -------------------------------------------------------------------------------- 1 | Reset(); 36 | } 37 | 38 | public function __destruct() 39 | { 40 | $this->Disconnect(); 41 | } 42 | 43 | public function Reset() 44 | { 45 | $this->fp = false; 46 | $this->client = true; 47 | $this->extensions = array(); 48 | $this->csprng = false; 49 | $this->state = self::STATE_CONNECTING; 50 | $this->closemode = self::CLOSE_IMMEDIATELY; 51 | $this->readdata = ""; 52 | $this->maxreadframesize = 2000000; 53 | $this->readmessages = array(); 54 | $this->maxreadmessagesize = 10000000; 55 | $this->writedata = ""; 56 | $this->writemessages = array(); 57 | $this->keepalive = 30; 58 | $this->lastkeepalive = time(); 59 | $this->keepalivesent = false; 60 | $this->rawrecvsize = 0; 61 | $this->rawsendsize = 0; 62 | } 63 | 64 | public function SetServerMode() 65 | { 66 | $this->client = false; 67 | } 68 | 69 | public function SetClientMode() 70 | { 71 | $this->client = true; 72 | } 73 | 74 | public function SetExtensions($extensions) 75 | { 76 | $this->extensions = (array)$extensions; 77 | } 78 | 79 | public function SetCloseMode($mode) 80 | { 81 | $this->closemode = $mode; 82 | } 83 | 84 | public function SetKeepAliveTimeout($keepalive) 85 | { 86 | $this->keepalive = (int)$keepalive; 87 | } 88 | 89 | public function GetKeepAliveTimeout() 90 | { 91 | return $this->keepalive; 92 | } 93 | 94 | public function SetMaxReadFrameSize($maxsize) 95 | { 96 | $this->maxreadframesize = (is_bool($maxsize) ? false : (int)$maxsize); 97 | } 98 | 99 | public function SetMaxReadMessageSize($maxsize) 100 | { 101 | $this->maxreadmessagesize = (is_bool($maxsize) ? false : (int)$maxsize); 102 | } 103 | 104 | public function GetRawRecvSize() 105 | { 106 | return $this->rawrecvsize; 107 | } 108 | 109 | public function GetRawSendSize() 110 | { 111 | return $this->rawsendsize; 112 | } 113 | 114 | public function Connect($url, $origin, $options = array(), $web = false) 115 | { 116 | $this->Disconnect(); 117 | 118 | if (class_exists("CSPRNG", false) && $this->csprng === false) $this->csprng = new CSPRNG(); 119 | 120 | if (isset($options["connected_fp"]) && is_resource($options["connected_fp"])) $this->fp = $options["connected_fp"]; 121 | else 122 | { 123 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 124 | 125 | // Use WebBrowser to initiate the connection. 126 | if ($web === false) $web = new WebBrowser(); 127 | 128 | // Transform URL. 129 | $url2 = HTTP::ExtractURL($url); 130 | if ($url2["scheme"] != "ws" && $url2["scheme"] != "wss") return array("success" => false, "error" => self::WSTranslate("WebSocket::Connect() only supports the 'ws' and 'wss' protocols."), "errorcode" => "protocol_check"); 131 | $url2["scheme"] = str_replace("ws", "http", $url2["scheme"]); 132 | $url2 = HTTP::CondenseURL($url2); 133 | 134 | // Generate correct request headers. 135 | if (!isset($options["headers"])) $options["headers"] = array(); 136 | $options["headers"]["Connection"] = "keep-alive, Upgrade"; 137 | if ($origin != "") $options["headers"]["Origin"] = $origin; 138 | $options["headers"]["Pragma"] = "no-cache"; 139 | $key = base64_encode($this->PRNGBytes(16)); 140 | $options["headers"]["Sec-WebSocket-Key"] = $key; 141 | $options["headers"]["Sec-WebSocket-Version"] = "13"; 142 | $options["headers"]["Upgrade"] = "websocket"; 143 | 144 | // No async support for connecting at this time. Async mode is enabled AFTER connecting though. 145 | unset($options["async"]); 146 | 147 | // Connect to the WebSocket. 148 | $result = $web->Process($url2, $options); 149 | if (!$result["success"]) return $result; 150 | if ($result["response"]["code"] != 101) return array("success" => false, "error" => self::WSTranslate("WebSocket::Connect() failed to connect to the WebSocket. Server returned: %s %s", $result["response"]["code"], $result["response"]["meaning"]), "errorcode" => "incorrect_server_response"); 151 | if (!isset($result["headers"]["Sec-Websocket-Accept"])) return array("success" => false, "error" => self::WSTranslate("Server failed to include a 'Sec-WebSocket-Accept' header in its response to the request."), "errorcode" => "missing_server_websocket_accept_header"); 152 | 153 | // Verify the Sec-WebSocket-Accept response. 154 | if ($result["headers"]["Sec-Websocket-Accept"][0] !== base64_encode(sha1($key . self::KEY_GUID, true))) return array("success" => false, "error" => self::WSTranslate("The server's 'Sec-WebSocket-Accept' header is invalid."), "errorcode" => "invalid_server_websocket_accept_header"); 155 | 156 | $this->fp = $result["fp"]; 157 | } 158 | 159 | // Enable non-blocking mode. 160 | stream_set_blocking($this->fp, 0); 161 | 162 | $this->state = self::STATE_OPEN; 163 | 164 | $this->readdata = ""; 165 | $this->readmessages = array(); 166 | $this->writedata = ""; 167 | $this->writemessages = array(); 168 | $this->lastkeepalive = time(); 169 | $this->keepalivesent = false; 170 | $this->rawrecvsize = 0; 171 | $this->rawsendsize = 0; 172 | 173 | return array("success" => true); 174 | } 175 | 176 | public function Disconnect() 177 | { 178 | if ($this->fp !== false && $this->state === self::STATE_OPEN) 179 | { 180 | if ($this->closemode === self::CLOSE_IMMEDIATELY) $this->writemessages = array(); 181 | else if ($this->closemode === self::CLOSE_AFTER_CURRENT_MESSAGE) $this->writemessages = array_slice($this->writemessages, 0, 1); 182 | 183 | $this->state = self::STATE_CLOSE; 184 | 185 | $this->Write("", self::FRAMETYPE_CONNECTION_CLOSE, true, $this->client); 186 | 187 | $this->Wait($this->client ? false : 0); 188 | } 189 | 190 | if ($this->fp !== false) 191 | { 192 | @fclose($this->fp); 193 | 194 | $this->fp = false; 195 | } 196 | 197 | $this->state = self::STATE_CONNECTING; 198 | $this->readdata = ""; 199 | $this->readmessages = array(); 200 | $this->writedata = ""; 201 | $this->writemessages = array(); 202 | $this->lastkeepalive = time(); 203 | $this->keepalivesent = false; 204 | } 205 | 206 | // Reads the next message or message fragment (depending on $finished). Returns immediately unless $wait is not false. 207 | public function Read($finished = true, $wait = false) 208 | { 209 | if ($this->fp === false || $this->state === self::STATE_CONNECTING) return array("success" => false, "error" => self::WSTranslate("Connection not established."), "errorcode" => "no_connection"); 210 | 211 | if ($wait) 212 | { 213 | while (!count($this->readmessages) || ($finished && !$this->readmessages[0]["fin"])) 214 | { 215 | $result = $this->Wait(); 216 | if (!$result["success"]) return $result; 217 | } 218 | } 219 | 220 | $data = false; 221 | 222 | if (count($this->readmessages)) 223 | { 224 | if ($finished) 225 | { 226 | if ($this->readmessages[0]["fin"]) $data = array_shift($this->readmessages); 227 | } 228 | else 229 | { 230 | $data = $this->readmessages[0]; 231 | 232 | $this->readmessages[0]["payload"] = ""; 233 | } 234 | } 235 | 236 | return array("success" => true, "data" => $data); 237 | } 238 | 239 | // Adds the message to the write queue. Returns immediately unless $wait is not false. 240 | public function Write($message, $frametype, $last = true, $wait = false, $pos = false) 241 | { 242 | if ($this->fp === false || $this->state === self::STATE_CONNECTING) return array("success" => false, "error" => self::WSTranslate("Connection not established."), "errorcode" => "no_connection"); 243 | 244 | $message = (string)$message; 245 | 246 | $y = count($this->writemessages); 247 | $lastcompleted = (!$y || $this->writemessages[$y - 1]["fin"]); 248 | if ($frametype >= 0x08 || $lastcompleted) 249 | { 250 | if ($frametype >= 0x08) $last = true; 251 | else $pos = false; 252 | 253 | $frame = array( 254 | "fin" => (bool)$last, 255 | "framessent" => 0, 256 | "opcode" => $frametype, 257 | "payloads" => array($message) 258 | ); 259 | 260 | array_splice($this->writemessages, ($pos !== false ? $pos : $y), 0, array($frame)); 261 | } 262 | else 263 | { 264 | if ($frametype !== $this->writemessages[$y - 1]["opcode"]) return array("success" => false, "error" => self::WSTranslate("Mismatched frame type (opcode) specified."), "errorcode" => "mismatched_frame_type"); 265 | 266 | $this->writemessages[$y - 1]["fin"] = (bool)$last; 267 | $this->writemessages[$y - 1]["payloads"][] = $message; 268 | } 269 | 270 | if ($wait) 271 | { 272 | while ($this->NeedsWrite()) 273 | { 274 | $result = $this->Wait(); 275 | if (!$result["success"]) return $result; 276 | } 277 | } 278 | 279 | return array("success" => true); 280 | } 281 | 282 | public function NeedsWrite() 283 | { 284 | $this->FillWriteData(); 285 | 286 | return ($this->writedata !== ""); 287 | } 288 | 289 | public function NumWriteMessages() 290 | { 291 | return count($this->writemessages); 292 | } 293 | 294 | // Dangerous but allows for stream_select() calls on multiple, separate stream handles. 295 | public function GetStream() 296 | { 297 | return $this->fp; 298 | } 299 | 300 | // Waits until one or more events time out, handles reading and writing, processes the queues (handle control types automatically), and returns the latest status. 301 | public function Wait($timeout = false) 302 | { 303 | if ($this->fp === false || $this->state === self::STATE_CONNECTING) return array("success" => false, "error" => self::WSTranslate("Connection not established."), "errorcode" => "no_connection"); 304 | 305 | $result = $this->ProcessReadData(); 306 | if (!$result["success"]) return $result; 307 | 308 | $this->FillWriteData(); 309 | 310 | $readfp = array($this->fp); 311 | $writefp = ($this->writedata !== "" ? array($this->fp) : NULL); 312 | $exceptfp = NULL; 313 | if ($timeout === false || $timeout > $this->keepalive) $timeout = $this->keepalive; 314 | $result = @stream_select($readfp, $writefp, $exceptfp, $timeout); 315 | if ($result === false) return array("success" => false, "error" => self::WSTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 316 | 317 | // Process queues and timeouts. 318 | $result = $this->ProcessQueuesAndTimeoutState(($result > 0 && count($readfp)), ($result > 0 && $writefp !== NULL && count($writefp))); 319 | 320 | return $result; 321 | } 322 | 323 | // A mostly internal function. Useful for managing multiple simultaneous WebSocket connections. 324 | public function ProcessQueuesAndTimeoutState($read, $write, $readsize = 65536) 325 | { 326 | if ($this->fp === false || $this->state === self::STATE_CONNECTING) return array("success" => false, "error" => self::WSTranslate("Connection not established."), "errorcode" => "no_connection"); 327 | 328 | if ($read) 329 | { 330 | $result = @fread($this->fp, $readsize); 331 | if ($result === false || ($result === "" && feof($this->fp))) return array("success" => false, "error" => self::WSTranslate("ProcessQueuesAndTimeoutState() failed due to fread() failure. Most likely cause: Connection failure."), "errorcode" => "fread_failed"); 332 | 333 | if ($result !== "") 334 | { 335 | $this->rawrecvsize += strlen($result); 336 | $this->readdata .= $result; 337 | 338 | if ($this->maxreadframesize !== false && strlen($this->readdata) > $this->maxreadframesize) return array("success" => false, "error" => self::WSTranslate("ProcessQueuesAndTimeoutState() failed due to peer sending a single frame exceeding %s bytes of data.", $this->maxreadframesize), "errorcode" => "max_read_frame_size_exceeded"); 339 | 340 | $result = $this->ProcessReadData(); 341 | if (!$result["success"]) return $result; 342 | 343 | $this->lastkeepalive = time(); 344 | $this->keepalivesent = false; 345 | } 346 | } 347 | 348 | if ($write) 349 | { 350 | $result = @fwrite($this->fp, $this->writedata); 351 | if ($result === false || ($this->writedata === "" && feof($this->fp))) return array("success" => false, "error" => self::WSTranslate("ProcessQueuesAndTimeoutState() failed due to fwrite() failure. Most likely cause: Connection failure."), "errorcode" => "fwrite_failed"); 352 | 353 | if ($result) 354 | { 355 | $this->rawsendsize += $result; 356 | $this->writedata = (string)substr($this->writedata, $result); 357 | 358 | $this->lastkeepalive = time(); 359 | $this->keepalivesent = false; 360 | } 361 | } 362 | 363 | // Handle timeout state. 364 | if ($this->lastkeepalive < time() - $this->keepalive) 365 | { 366 | if ($this->keepalivesent) return array("success" => false, "error" => self::WSTranslate("ProcessQueuesAndTimeoutState() failed due to non-response from peer to ping frame. Most likely cause: Connection failure."), "errorcode" => "ping_failed"); 367 | else 368 | { 369 | $result = $this->Write(time(), self::FRAMETYPE_PING, true, false, 0); 370 | if (!$result["success"]) return $result; 371 | 372 | $this->lastkeepalive = time(); 373 | $this->keepalivesent = true; 374 | } 375 | } 376 | 377 | return array("success" => true); 378 | } 379 | 380 | protected function ProcessReadData() 381 | { 382 | while (($frame = $this->ReadFrame()) !== false) 383 | { 384 | // Verify that the opcode is probably valid. 385 | if (($frame["opcode"] >= 0x03 && $frame["opcode"] <= 0x07) || $frame["opcode"] >= 0x0B) return array("success" => false, "error" => self::WSTranslate("Invalid frame detected. Bad opcode 0x%02X.", $frame["opcode"]), "errorcode" => "bad_frame_opcode"); 386 | 387 | // No extension support (yet). 388 | if ($frame["rsv1"] || $frame["rsv2"] || $frame["rsv3"]) return array("success" => false, "error" => self::WSTranslate("Invalid frame detected. One or more reserved extension bits are set."), "errorcode" => "bad_reserved_bits_set"); 389 | 390 | if ($frame["opcode"] >= 0x08) 391 | { 392 | // Handle the control frame. 393 | if (!$frame["fin"]) return array("success" => false, "error" => self::WSTranslate("Invalid frame detected. Fragmented control frame was received."), "errorcode" => "bad_control_frame"); 394 | 395 | if ($frame["opcode"] === self::FRAMETYPE_CONNECTION_CLOSE) 396 | { 397 | if ($this->state === self::STATE_CLOSE) 398 | { 399 | // Already sent the close state. 400 | @fclose($this->fp); 401 | $this->fp = false; 402 | 403 | return array("success" => false, "error" => self::WSTranslate("Connection closed by peer."), "errorcode" => "connection_closed"); 404 | } 405 | else 406 | { 407 | // Change the state to close and send the connection close response to the peer at the appropriate time. 408 | if ($this->closemode === self::CLOSE_IMMEDIATELY) $this->writemessages = array(); 409 | else if ($this->closemode === self::CLOSE_AFTER_CURRENT_MESSAGE) $this->writemessages = array_slice($this->writemessages, 0, 1); 410 | 411 | $this->state = self::STATE_CLOSE; 412 | 413 | $result = $this->Write("", self::FRAMETYPE_CONNECTION_CLOSE); 414 | if (!$result["success"]) return $result; 415 | } 416 | } 417 | else if ($frame["opcode"] === self::FRAMETYPE_PING) 418 | { 419 | if ($this->state !== self::STATE_CLOSE) 420 | { 421 | // Received a ping. Respond with a pong with the same payload. 422 | $result = $this->Write($frame["payload"], self::FRAMETYPE_PONG, true, false, 0); 423 | if (!$result["success"]) return $result; 424 | } 425 | } 426 | else if ($frame["opcode"] === self::FRAMETYPE_PONG) 427 | { 428 | // Do nothing. 429 | } 430 | } 431 | else 432 | { 433 | // Add this frame to the read message queue. 434 | $lastcompleted = (!count($this->readmessages) || $this->readmessages[count($this->readmessages) - 1]["fin"]); 435 | if ($lastcompleted) 436 | { 437 | // Make sure the new frame is the start of a fragment or is not fragemented. 438 | if ($frame["opcode"] === self::FRAMETYPE_CONTINUATION) return array("success" => false, "error" => self::WSTranslate("Invalid frame detected. Fragment continuation frame was received at the start of a fragment."), "errorcode" => "bad_continuation_frame"); 439 | 440 | $this->readmessages[] = $frame; 441 | } 442 | else 443 | { 444 | // Make sure the frame is a continuation frame. 445 | if ($frame["opcode"] !== self::FRAMETYPE_CONTINUATION) return array("success" => false, "error" => self::WSTranslate("Invalid frame detected. Fragment continuation frame was not received for a fragment."), "errorcode" => "missing_continuation_frame"); 446 | 447 | $this->readmessages[count($this->readmessages) - 1]["fin"] = $frame["fin"]; 448 | $this->readmessages[count($this->readmessages) - 1]["payload"] .= $frame["payload"]; 449 | } 450 | 451 | if ($this->maxreadmessagesize !== false && strlen($this->readmessages[count($this->readmessages) - 1]["payload"]) > $this->maxreadmessagesize) return array("success" => false, "error" => self::WSTranslate("Peer sent a single message exceeding %s bytes of data.", $this->maxreadmessagesize), "errorcode" => "max_read_message_size_exceeded"); 452 | } 453 | 454 | //var_dump($frame); 455 | } 456 | 457 | return array("success" => true); 458 | } 459 | 460 | // Parses the current input data to see if there is enough information to extract a single frame. 461 | // Does not do any error checking beyond loading the frame and decoding any masked data. 462 | protected function ReadFrame() 463 | { 464 | if (strlen($this->readdata) < 2) return false; 465 | 466 | $chr = ord($this->readdata[0]); 467 | $fin = (($chr & 0x80) ? true : false); 468 | $rsv1 = (($chr & 0x40) ? true : false); 469 | $rsv2 = (($chr & 0x20) ? true : false); 470 | $rsv3 = (($chr & 0x10) ? true : false); 471 | $opcode = $chr & 0x0F; 472 | 473 | $chr = ord($this->readdata[1]); 474 | $mask = (($chr & 0x80) ? true : false); 475 | $length = $chr & 0x7F; 476 | if ($length == 126) $start = 4; 477 | else if ($length == 127) $start = 10; 478 | else $start = 2; 479 | 480 | if (strlen($this->readdata) < $start + ($mask ? 4 : 0)) return false; 481 | 482 | // Frame minus payload calculated. 483 | if ($length == 126) $length = self::UnpackInt(substr($this->readdata, 2, 2)); 484 | else if ($length == 127) $length = self::UnpackInt(substr($this->readdata, 2, 8)); 485 | 486 | if ($mask) 487 | { 488 | $maskingkey = substr($this->readdata, $start, 4); 489 | $start += 4; 490 | } 491 | 492 | if (strlen($this->readdata) < $start + $length) return false; 493 | 494 | $payload = substr($this->readdata, $start, $length); 495 | 496 | $this->readdata = substr($this->readdata, $start + $length); 497 | 498 | if ($mask) 499 | { 500 | // Decode the payload. 501 | for ($x = 0; $x < $length; $x++) 502 | { 503 | $payload[$x] = chr(ord($payload[$x]) ^ ord($maskingkey[$x % 4])); 504 | } 505 | } 506 | 507 | $result = array( 508 | "fin" => $fin, 509 | "rsv1" => $rsv1, 510 | "rsv2" => $rsv2, 511 | "rsv3" => $rsv3, 512 | "opcode" => $opcode, 513 | "mask" => $mask, 514 | "payload" => $payload 515 | ); 516 | 517 | return $result; 518 | } 519 | 520 | // Converts messages in the queue to a data stream of frames. 521 | protected function FillWriteData() 522 | { 523 | while (strlen($this->writedata) < 65536 && count($this->writemessages) && count($this->writemessages[0]["payloads"])) 524 | { 525 | $payload = array_shift($this->writemessages[0]["payloads"]); 526 | 527 | $fin = ($this->writemessages[0]["fin"] && !count($this->writemessages[0]["payloads"])); 528 | 529 | if ($this->writemessages[0]["framessent"] === 0) $opcode = $this->writemessages[0]["opcode"]; 530 | else $opcode = self::FRAMETYPE_CONTINUATION; 531 | 532 | $this->WriteFrame($fin, $opcode, $payload); 533 | 534 | $this->writemessages[0]["framessent"]++; 535 | 536 | if ($fin) array_shift($this->writemessages); 537 | } 538 | } 539 | 540 | // Generates the actual frame data to be sent. 541 | protected function WriteFrame($fin, $opcode, $payload) 542 | { 543 | $rsv1 = false; 544 | $rsv2 = false; 545 | $rsv3 = false; 546 | $mask = $this->client; 547 | 548 | $data = chr(($fin ? 0x80 : 0x00) | ($rsv1 ? 0x40 : 0x00) | ($rsv2 ? 0x20 : 0x00) | ($rsv3 ? 0x10 : 0x00) | ($opcode & 0x0F)); 549 | 550 | if (strlen($payload) < 126) $length = strlen($payload); 551 | else if (strlen($payload) < 65536) $length = 126; 552 | else $length = 127; 553 | 554 | $data .= chr(($mask ? 0x80 : 0x00) | ($length & 0x7F)); 555 | 556 | if ($length === 126) $data .= pack("n", strlen($payload)); 557 | else if ($length === 127) $data .= self::PackInt64(strlen($payload)); 558 | 559 | if ($mask) 560 | { 561 | $maskingkey = $this->PRNGBytes(4); 562 | $data .= $maskingkey; 563 | 564 | // Encode the payload. 565 | $y = strlen($payload); 566 | for ($x = 0; $x < $y; $x++) 567 | { 568 | $payload[$x] = chr(ord($payload[$x]) ^ ord($maskingkey[$x % 4])); 569 | } 570 | } 571 | 572 | $data .= $payload; 573 | 574 | $this->writedata .= $data; 575 | } 576 | 577 | // This function follows the specification IF CSPRNG is available, but it isn't necessary to do so. 578 | protected function PRNGBytes($length) 579 | { 580 | if ($this->csprng !== false) $result = $this->csprng->GetBytes($length); 581 | else 582 | { 583 | $result = ""; 584 | while (strlen($result) < $length) $result .= chr(mt_rand(0, 255)); 585 | } 586 | 587 | return $result; 588 | } 589 | 590 | public static function UnpackInt($data) 591 | { 592 | if ($data === false) return false; 593 | 594 | if (strlen($data) == 2) $result = unpack("n", $data); 595 | else if (strlen($data) == 4) $result = unpack("N", $data); 596 | else if (strlen($data) == 8) 597 | { 598 | $result = 0; 599 | for ($x = 0; $x < 8; $x++) 600 | { 601 | $result = ($result * 256) + ord($data[$x]); 602 | } 603 | 604 | return $result; 605 | } 606 | else return false; 607 | 608 | return $result[1]; 609 | } 610 | 611 | public static function PackInt64($num) 612 | { 613 | $result = ""; 614 | 615 | if (is_int(2147483648)) $floatlim = 9223372036854775808; 616 | else $floatlim = 2147483648; 617 | 618 | if (is_float($num)) 619 | { 620 | $num = floor($num); 621 | if ($num < (double)$floatlim) $num = (int)$num; 622 | } 623 | 624 | while (is_float($num)) 625 | { 626 | $byte = (int)fmod($num, 256); 627 | $result = chr($byte) . $result; 628 | 629 | $num = floor($num / 256); 630 | if (is_float($num) && $num < (double)$floatlim) $num = (int)$num; 631 | } 632 | 633 | while ($num > 0) 634 | { 635 | $byte = $num & 0xFF; 636 | $result = chr($byte) . $result; 637 | $num = $num >> 8; 638 | } 639 | 640 | $result = str_pad($result, 8, "\x00", STR_PAD_LEFT); 641 | $result = substr($result, -8); 642 | 643 | return $result; 644 | } 645 | 646 | public static function WSTranslate() 647 | { 648 | $args = func_get_args(); 649 | if (!count($args)) return ""; 650 | 651 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 652 | } 653 | } 654 | ?> -------------------------------------------------------------------------------- /tests/test_dns_over_https.php: -------------------------------------------------------------------------------- 1 | Process($url); 32 | if (!$result["success"] || $result["response"]["code"] != 200) 33 | { 34 | var_dump($result); 35 | 36 | exit(); 37 | } 38 | 39 | $html = TagFilter::Explode($result["body"], $htmloptions); 40 | $root = $html->Get(); 41 | 42 | echo "Page title: " . $root->Find("title")->current()->GetPlainText() . "\n\n"; 43 | } 44 | 45 | echo "DNS over HTTPS query results:\n"; 46 | echo json_encode($web->GetDOHCache(), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n"; 47 | ?> -------------------------------------------------------------------------------- /tests/test_interactive.php: -------------------------------------------------------------------------------- 1 | true, "httpopts" => array("pre_retrievewebpage_callback" => "DisplayInteractiveRequest"))); 31 | $filteropts = TagFilter::GetHTMLOptions(); 32 | 33 | $url = false; 34 | do 35 | { 36 | if ($url === false) 37 | { 38 | echo "URL: "; 39 | $url = trim(fgets(STDIN)); 40 | 41 | $result = array( 42 | "url" => $url, 43 | "options" => array() 44 | ); 45 | } 46 | 47 | $result2 = $web->Process($result["url"], $result["options"]); 48 | if (!$result2["success"]) 49 | { 50 | echo "An error occurred. " . $result2["error"] . "\n"; 51 | 52 | $url = false; 53 | } 54 | else if ($result2["response"]["code"] != 200) 55 | { 56 | echo "An unexpected response code was returned. " . $result2["response"]["line"] . "\n"; 57 | 58 | $url = false; 59 | } 60 | else 61 | { 62 | // Clean up the HTML. 63 | $body = TagFilter::Run($result2["body"], $filteropts); 64 | $html->load($body); 65 | 66 | $links = array(); 67 | $rows = $html->find('a[href]'); 68 | foreach ($rows as $row) 69 | { 70 | $links[] = array("url" => HTTP::ConvertRelativeToAbsoluteURL($result2["url"], (string)$row->href), "display" => trim($row->plaintext)); 71 | } 72 | 73 | echo "Available links:\n"; 74 | foreach ($links as $num => $info) 75 | { 76 | echo "\t" . ($num + 1) . ": " . ($info["display"] !== "" ? $info["display"] : $info["url"]) . "\n"; 77 | } 78 | echo "\n"; 79 | 80 | if (count($result2["forms"])) echo "Available forms: " . count($result2["forms"]) . "\n\n"; 81 | 82 | do 83 | { 84 | echo "Command (URL, link ID, 'forms', 'body', or 'exit'): "; 85 | $cmd = trim(fgets(STDIN)); 86 | 87 | if ($cmd === "body") echo "\n" . $body . "\n\n"; 88 | 89 | } while ($cmd != "forms" && $cmd != "exit" && strpos($cmd, "/") === false && !isset($links[(int)$cmd - 1])); 90 | 91 | if ($cmd === "exit") exit(); 92 | else if ($cmd === "forms") $result = $web->InteractiveFormFill($result2["forms"]); 93 | else if (strpos($cmd, "/") !== false) 94 | { 95 | $result = array( 96 | "url" => HTTP::ConvertRelativeToAbsoluteURL($result2["url"], $cmd), 97 | "options" => array() 98 | ); 99 | } 100 | else 101 | { 102 | $result = array( 103 | "url" => $links[(int)$cmd - 1]["url"], 104 | "options" => array() 105 | ); 106 | } 107 | } 108 | } while (1); 109 | ?> -------------------------------------------------------------------------------- /tests/test_suite.php: -------------------------------------------------------------------------------- 1 | $token) 34 | { 35 | echo " " . ($num + 1) . ": " . json_encode($token, JSON_UNESCAPED_SLASHES) . "\n"; 36 | } 37 | 38 | echo "\n"; 39 | } 40 | 41 | echo "Testing CSS selector parsing\n"; 42 | echo "----------------------------\n"; 43 | DisplayParseSelectorResult(TagFilter::ParseSelector("li,p")); 44 | DisplayParseSelectorResult(TagFilter::ParseSelector("li , p")); 45 | DisplayParseSelectorResult(TagFilter::ParseSelector("li,p,")); 46 | DisplayParseSelectorResult(TagFilter::ParseSelector("*,*.t1")); 47 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[href]")); 48 | DisplayParseSelectorResult(TagFilter::ParseSelector("a [href]")); 49 | DisplayParseSelectorResult(TagFilter::ParseSelector('a[href="http://domain.com/"]')); 50 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[href='http://domain.com/']")); 51 | DisplayParseSelectorResult(TagFilter::ParseSelector("tr.gotclass")); 52 | DisplayParseSelectorResult(TagFilter::ParseSelector("tr[class~=gotclass]")); 53 | DisplayParseSelectorResult(TagFilter::ParseSelector("span[lang|=en]")); 54 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[ href ^= 'http://' ]")); 55 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[href$='.jpg']")); 56 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[href*='/images/']")); 57 | DisplayParseSelectorResult(TagFilter::ParseSelector("tr.gotclass.moreclass.moarclass")); 58 | DisplayParseSelectorResult(TagFilter::ParseSelector(".t1:not(.t2)")); 59 | DisplayParseSelectorResult(TagFilter::ParseSelector(":not(.t2).t1")); 60 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(.t1):not(.t2)")); 61 | DisplayParseSelectorResult(TagFilter::ParseSelector("#the-one_id")); 62 | DisplayParseSelectorResult(TagFilter::ParseSelector("#the-one_id#nevermatch")); 63 | DisplayParseSelectorResult(TagFilter::ParseSelector("a#the-one_id.gotclass")); 64 | DisplayParseSelectorResult(TagFilter::ParseSelector("a[href]a:not(p)")); 65 | DisplayParseSelectorResult(TagFilter::ParseSelector("p *:link")); 66 | DisplayParseSelectorResult(TagFilter::ParseSelector("p *:visited")); 67 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:target")); 68 | DisplayParseSelectorResult(TagFilter::ParseSelector("li:lang(en-US)")); 69 | DisplayParseSelectorResult(TagFilter::ParseSelector("input:enabled,button:disabled")); 70 | DisplayParseSelectorResult(TagFilter::ParseSelector("input:checked, input:checked + span")); 71 | DisplayParseSelectorResult(TagFilter::ParseSelector("ul > li:nth-child(odd)")); 72 | DisplayParseSelectorResult(TagFilter::ParseSelector("ol > li:nth-child(even)")); 73 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable tr:nth-child(-n+4)")); 74 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:nth-child(3n+1)")); 75 | DisplayParseSelectorResult(TagFilter::ParseSelector("ul > li:nth-last-child(odd)")); 76 | DisplayParseSelectorResult(TagFilter::ParseSelector("ol > li:nth-last-child(even)")); 77 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable tr:nth-last-child(-n+4)")); 78 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:nth-last-child(3n+1)")); 79 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:nth-of-type(3)")); 80 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:nth-last-of-type(3)")); 81 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:first-child")); 82 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:last-child")); 83 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:first-of-type")); 84 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:last-of-type")); 85 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:only-child")); 86 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:only-of-type")); 87 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:first-line")); 88 | DisplayParseSelectorResult(TagFilter::ParseSelector("p::first-line")); 89 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:first-letter")); 90 | DisplayParseSelectorResult(TagFilter::ParseSelector("p::first-letter")); 91 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:before")); 92 | DisplayParseSelectorResult(TagFilter::ParseSelector("p::before")); 93 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:after")); 94 | DisplayParseSelectorResult(TagFilter::ParseSelector("p::after")); 95 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv p")); 96 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv > p")); 97 | DisplayParseSelectorResult(TagFilter::ParseSelector(".test > div")); 98 | DisplayParseSelectorResult(TagFilter::ParseSelector("#test > div")); 99 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv p + p")); 100 | DisplayParseSelectorResult(TagFilter::ParseSelector(".nope + p")); 101 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv > p ~ p")); 102 | DisplayParseSelectorResult(TagFilter::ParseSelector('div.maindiv *:not([title^="super "])')); 103 | DisplayParseSelectorResult(TagFilter::ParseSelector('div.maindiv *:not([title*=" ion "])')); 104 | DisplayParseSelectorResult(TagFilter::ParseSelector('div.maindiv *:not([title$=" cannon"])')); 105 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(#badid)")); 106 | DisplayParseSelectorResult(TagFilter::ParseSelector(":not(:link)")); 107 | DisplayParseSelectorResult(TagFilter::ParseSelector(":not(:visited)")); 108 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:target)")); 109 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:lang(fr))")); 110 | DisplayParseSelectorResult(TagFilter::ParseSelector("input:not(:enabled),button:not(:disabled)")); 111 | DisplayParseSelectorResult(TagFilter::ParseSelector("input:not(:checked), input:not(:checked) + span")); 112 | DisplayParseSelectorResult(TagFilter::ParseSelector("ul > li:not(:nth-child(odd))")); 113 | DisplayParseSelectorResult(TagFilter::ParseSelector("ol > li:not(:nth-child(even))")); 114 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable tr:not(:nth-child(-n+4))")); 115 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:not(:nth-child(3n+1))")); 116 | DisplayParseSelectorResult(TagFilter::ParseSelector("ul > li:not(:nth-last-child(odd))")); 117 | DisplayParseSelectorResult(TagFilter::ParseSelector("ol > li:not(:nth-last-child(even))")); 118 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable tr:not(:nth-last-child(-n+4))")); 119 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:not(:nth-last-child(3n+1))")); 120 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:nth-of-type(3))")); 121 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:nth-last-of-type(3))")); 122 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:not(:first-child)")); 123 | DisplayParseSelectorResult(TagFilter::ParseSelector("table.maintable td:not(:last-child)")); 124 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:first-of-type)")); 125 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:last-of-type)")); 126 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:only-child)")); 127 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:only-of-type)")); 128 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(:not(p))")); 129 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv > div.inside p")); 130 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv + div.other ~ p")); 131 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv + div.other p")); 132 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv div.inside > p")); 133 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv ~ div.other + p")); 134 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:empty")); 135 | DisplayParseSelectorResult(TagFilter::ParseSelector("input[form],textarea[form],select[form],button[form],datalist[id]")); 136 | 137 | // Start of ugly/error tests. 138 | DisplayParseSelectorResult(TagFilter::ParseSelector(".5cm")); 139 | DisplayParseSelectorResult(TagFilter::ParseSelector('.\5cm')); 140 | DisplayParseSelectorResult(TagFilter::ParseSelector('.impossible\ class\ name')); 141 | DisplayParseSelectorResult(TagFilter::ParseSelector('.escaped\.class\.name')); 142 | DisplayParseSelectorResult(TagFilter::ParseSelector("div.maindiv & div.inner, p")); 143 | DisplayParseSelectorResult(TagFilter::ParseSelector("[*=nope]")); 144 | DisplayParseSelectorResult(TagFilter::ParseSelector("[*|*=nope]")); 145 | DisplayParseSelectorResult(TagFilter::ParseSelector("[*|fo-reals*=yup]")); 146 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:subject")); 147 | DisplayParseSelectorResult(TagFilter::ParseSelector("\\0050\r\n:\\04e\r\n\\6F \\000054( \\70 \r\n\t)")); // p:not(p) in Unicode 148 | DisplayParseSelectorResult(TagFilter::ParseSelector("p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p,p")); 149 | DisplayParseSelectorResult(TagFilter::ParseSelector(".p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p,.p")); 150 | DisplayParseSelectorResult(TagFilter::ParseSelector(".p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p.p")); 151 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span):not(.span)")); 152 | DisplayParseSelectorResult(TagFilter::ParseSelector("p:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child:first-child")); 153 | DisplayParseSelectorResult(TagFilter::ParseSelector(".13")); 154 | DisplayParseSelectorResult(TagFilter::ParseSelector('.\13')); 155 | DisplayParseSelectorResult(TagFilter::ParseSelector('.\13 \33')); 156 | DisplayParseSelectorResult(TagFilter::ParseSelector('div:not(#other).notclass:not(.nope).test#theid#theid')); 157 | DisplayParseSelectorResult(TagFilter::ParseSelector('DIV TABLE p')); 158 | DisplayParseSelectorResult(TagFilter::ParseSelector('p..nope')); 159 | DisplayParseSelectorResult(TagFilter::ParseSelector('p[class$=""]')); 160 | DisplayParseSelectorResult(TagFilter::ParseSelector('p[class^=""]')); 161 | DisplayParseSelectorResult(TagFilter::ParseSelector('p[class*=""]')); 162 | DisplayParseSelectorResult(TagFilter::ParseSelector('p:not([class$=""])')); 163 | DisplayParseSelectorResult(TagFilter::ParseSelector('p:not([class^=""])')); 164 | DisplayParseSelectorResult(TagFilter::ParseSelector('p:not([class*=""])')); 165 | echo "-------------------\n\n"; 166 | 167 | echo "Testing CSS selector cleanup\n"; 168 | echo "----------------------------\n"; 169 | $sel = "p.someclass.anotherclass#theid[attr][attr2=val]:first-child:not(a):not(.nope)#theid.someclass,@invalid"; 170 | echo $sel . "\n"; 171 | echo " Result: " . TagFilterNodes::MakeValidSelector($sel) . "\n\n"; 172 | $sel = "input:checked"; 173 | echo $sel . "\n"; 174 | echo " Result: " . TagFilterNodes::MakeValidSelector($sel) . "\n\n"; 175 | $sel = "div div.someclass > p ~ p"; 176 | echo $sel . "\n"; 177 | echo " Result: " . TagFilterNodes::MakeValidSelector($sel) . "\n\n"; 178 | $sel = "span,SPAN,SpAn,sPaN"; 179 | echo $sel . "\n"; 180 | echo " Result: " . TagFilterNodes::MakeValidSelector($sel) . "\n\n"; 181 | echo "-------------------\n\n"; 182 | 183 | // TagFilter/TagFilterStream HTML parser tests. 184 | $htmloptions = TagFilter::GetHTMLOptions(); 185 | $purifyoptions = array( 186 | "allowed_tags" => "img,a,p,br,b,strong,i,ul,ol,li", 187 | "allowed_attrs" => array("img" => "src", "a" => "href,id", "p" => "class"), 188 | "required_attrs" => array("img" => "src", "a" => "href"), 189 | "allowed_classes" => array("p" => "allowedclass"), 190 | "remove_empty" => "b,strong,i,ul,ol,li" 191 | ); 192 | 193 | echo "Testing raw HTML cleanup\n"; 194 | echo "------------------------\n"; 195 | $testfile = file_get_contents($rootpath . "/test_xss.txt"); 196 | $pos = strpos($testfile, "@EXIT@"); 197 | if ($pos === false) $pos = strlen($testfile); 198 | $testfile = substr($testfile, 0, $pos); 199 | 200 | $result = TagFilter::Run($testfile, $htmloptions); 201 | echo $result . "\n\n"; 202 | echo "-------------------\n\n"; 203 | 204 | echo "Testing XSS removal\n"; 205 | echo "-------------------\n"; 206 | $testfile = file_get_contents($rootpath . "/test_xss.txt"); 207 | $pos = strpos($testfile, "@EXIT@"); 208 | if ($pos === false) $pos = strlen($testfile); 209 | $testfile = substr($testfile, 0, $pos); 210 | 211 | $result = TagFilter::HTMLPurify($testfile, $htmloptions, $purifyoptions); 212 | echo $result . "\n\n"; 213 | echo "-------------------\n\n"; 214 | 215 | echo "Testing Word HTML cleanup\n"; 216 | echo "-------------------------\n"; 217 | $testfile = file_get_contents($rootpath . "/test_word.txt"); 218 | $pos = strpos($testfile, "@EXIT@"); 219 | if ($pos === false) $pos = strlen($testfile); 220 | $testfile = substr($testfile, 0, $pos); 221 | 222 | $result = TagFilter::HTMLPurify($testfile, $htmloptions, $purifyoptions); 223 | echo $result . "\n\n"; 224 | echo "-------------------------\n\n"; 225 | 226 | 227 | // Exploded HTML extraction and TagFilterNodes tests. 228 | echo "Testing Word HTML explode\n"; 229 | echo "-------------------------\n"; 230 | $testfile = file_get_contents($rootpath . "/test_word.txt"); 231 | $pos = strpos($testfile, "@EXIT@"); 232 | if ($pos === false) $pos = strlen($testfile); 233 | $testfile = substr($testfile, 0, $pos); 234 | 235 | // Returns a TagFilterNodes object. 236 | $tfn = TagFilter::Explode($testfile, $htmloptions); 237 | echo count($tfn->nodes) . " nodes\n"; 238 | foreach ($tfn->nodes as $num => $node) 239 | { 240 | echo " " . $num . ": " . json_encode($node, JSON_UNESCAPED_SLASHES) . "\n"; 241 | } 242 | echo "\n"; 243 | echo "-------------------------\n\n"; 244 | 245 | function DisplayTFNFindResult($msg, $tfn, $result) 246 | { 247 | echo $msg . "\n"; 248 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 249 | foreach ($result["ids"] as $id) 250 | { 251 | echo " " . $id . ":\n"; 252 | echo " Tag: " . $tfn->GetTag($id) . "\n"; 253 | echo " Node: " . json_encode($tfn->nodes[$id], JSON_UNESCAPED_SLASHES) . "\n"; 254 | echo " Outer HTML: " . $tfn->GetOuterHTML($id) . "\n"; 255 | echo " Inner HTML: " . $tfn->GetInnerHTML($id) . "\n"; 256 | echo " Plain text: " . $tfn->GetPlainText($id) . "\n"; 257 | if (isset($tfn->nodes[$id]["attrs"]["href"])) echo " href: " . $tfn->nodes[$id]["attrs"]["href"] . "\n"; 258 | if (isset($tfn->nodes[$id]["attrs"]["src"])) echo " src: " . $tfn->nodes[$id]["attrs"]["src"] . "\n"; 259 | } 260 | 261 | echo "\n"; 262 | } 263 | 264 | function DisplayOOTFNFindResults($msg, $results) 265 | { 266 | echo $msg . "\n"; 267 | foreach ($results as $id => $row) 268 | { 269 | echo " " . $id . ":\n"; 270 | echo " Tag: " . $row->Tag($id) . "\n"; 271 | echo " Node: " . json_encode($row->Node(), JSON_UNESCAPED_SLASHES) . "\n"; 272 | echo " Outer HTML: " . $row->GetOuterHTML() . "\n"; 273 | echo " Inner HTML: " . $row->GetInnerHTML() . "\n"; 274 | echo " Plain text: " . $row->GetPlainText() . "\n"; 275 | if (isset($row->href)) echo " href: " . $row->href . "\n"; 276 | if (isset($row->src)) echo " src: " . $row->src . "\n"; 277 | } 278 | 279 | echo "\n"; 280 | } 281 | 282 | echo "Testing TagFilterNodes features\n"; 283 | echo "-------------------------------\n"; 284 | DisplayTFNFindResult("Find('a')", $tfn, $tfn->Find("a")); 285 | DisplayTFNFindResult("Filter(Find('a'), '[href]')", $tfn, $tfn->Filter($tfn->Find("a"), "[href]")); 286 | DisplayTFNFindResult("Find('a[href]')", $tfn, $tfn->Find("a[href]")); 287 | DisplayTFNFindResult("Find('p')", $tfn, $tfn->Find("p")); 288 | DisplayTFNFindResult("Filter(Find('p'), 'a[href]')", $tfn, $tfn->Filter($tfn->Find("p"), "a[href]")); 289 | DisplayTFNFindResult("Filter(Find('p'), '/contains:A link')", $tfn, $tfn->Filter($tfn->Find("p"), "/contains:A link")); 290 | DisplayTFNFindResult("Filter(Find('p'), '/~contains:a link')", $tfn, $tfn->Filter($tfn->Find("p"), "/~contains:a link")); 291 | 292 | echo "Appending a '' tag to every 'p' tag that contains a 'a[href]'\n"; 293 | $result = $tfn->Filter($tfn->Find("p"), "a[href]"); 294 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 295 | foreach ($result["ids"] as $id) echo " " . $id . ": " . ($tfn->Move("", $id, true) ? "true" : "false") . "\n"; 296 | echo "\n"; 297 | 298 | DisplayTFNFindResult("Find('img')", $tfn, $tfn->Find("img")); 299 | DisplayTFNFindResult("Filter(Find('p'), 'a[href]')", $tfn, $tfn->Filter($tfn->Find("p"), "a[href]")); 300 | 301 | echo "Moving all 'img' tags to be the first child of its parent element.\n"; 302 | $result = $tfn->Find("img"); 303 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 304 | foreach ($result["ids"] as $id) echo " " . $id . ": " . ($tfn->Move($id, $tfn->nodes[$id]["parent"], 0) ? "true" : "false") . "\n"; 305 | echo "\n"; 306 | 307 | DisplayTFNFindResult("Find('img')", $tfn, $tfn->Find("img")); 308 | DisplayTFNFindResult("Filter(Find('p'), 'img')", $tfn, $tfn->Filter($tfn->Find("p"), "img")); 309 | 310 | echo "Replacing all 'img' tags with '' using SetOuterHTML().\n"; 311 | $result = $tfn->Find("img"); 312 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 313 | foreach ($result["ids"] as $id) echo " " . $id . ": " . ($tfn->SetOuterHTML($id, "") ? "true" : "false") . "\n"; 314 | echo "\n"; 315 | 316 | DisplayTFNFindResult("Find('img')", $tfn, $tfn->Find("img")); 317 | DisplayTFNFindResult("Filter(Find('p'), 'img')", $tfn, $tfn->Filter($tfn->Find("p"), "img")); 318 | 319 | echo "Bolding links using SetInnerHTML().\n"; 320 | $result = $tfn->Find("a[href]"); 321 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 322 | foreach ($result["ids"] as $id) echo " " . $id . ": " . ($tfn->SetInnerHTML($id, "" . $tfn->GetInnerHTML($id) . "") ? "true" : "false") . "\n"; 323 | echo "\n"; 324 | 325 | DisplayTFNFindResult("Find('a')", $tfn, $tfn->Find("a")); 326 | DisplayTFNFindResult("Filter(Find('p'), 'a[href]')", $tfn, $tfn->Filter($tfn->Find("p"), "a[href]")); 327 | 328 | echo "Removing all 'span' tags but keeping all child nodes.\n"; 329 | $result = $tfn->Find("span"); 330 | echo " " . json_encode($result, JSON_UNESCAPED_SLASHES) . "\n"; 331 | foreach ($result["ids"] as $id) 332 | { 333 | $tfn->Remove($id, true); 334 | echo " " . $id . ": true\n"; 335 | } 336 | echo "\n"; 337 | 338 | DisplayTFNFindResult("Find('span')", $tfn, $tfn->Find("span")); 339 | DisplayTFNFindResult("Find('p')", $tfn, $tfn->Find("p")); 340 | 341 | DisplayTFNFindResult("Find('p:first-of-type')", $tfn, $tfn->Find("p:first-of-type")); 342 | DisplayTFNFindResult("Find('p:last-of-type')", $tfn, $tfn->Find("p:last-of-type")); 343 | DisplayTFNFindResult("Find('a b:only-of-type')", $tfn, $tfn->Find("a b:only-of-type")); 344 | DisplayTFNFindResult("Find('p:nth-of-type(1)')", $tfn, $tfn->Find("p:nth-of-type(1)")); 345 | DisplayTFNFindResult("Find('p:nth-last-of-type(2)')", $tfn, $tfn->Find("p:nth-last-of-type(2)")); 346 | 347 | echo "Testing object-oriented access.\n\n"; 348 | echo "Root node:\n"; 349 | $root = $tfn->Get(); 350 | var_dump($root); 351 | echo "\n"; 352 | 353 | echo "First link: " . $root->Find("a[href]")->current()->href . "\n\n"; 354 | DisplayOOTFNFindResults("Find('a[href]')", $root->Find("a[href]")); 355 | DisplayOOTFNFindResults("Find('p')->Filter('a[href]')", $root->Find("p")->Filter("a[href]")); 356 | 357 | echo "-------------------------\n\n"; 358 | 359 | 360 | // Web scraping. 361 | $html = new simple_html_dom(); 362 | 363 | $web = new WebBrowser(); 364 | $result = $web->Process("http://www.barebonescms.com/"); 365 | if (!$result["success"]) echo "[FAIL] An error occurred. " . $result["error"] . "\n"; 366 | else if ($result["response"]["code"] != 200) echo "[FAIL] An unexpected response code was returned. " . $result["response"]["line"] . "\n"; 367 | else 368 | { 369 | echo "[PASS] The expected response was returned.\n"; 370 | 371 | $html->load($result["body"]); 372 | $rows = $html->find('a[href]'); 373 | foreach ($rows as $row) 374 | { 375 | echo "\t" . HTTP::ConvertRelativeToAbsoluteURL($result["url"], $row->href) . "\n"; 376 | } 377 | } 378 | 379 | $result = $web->Process("https://www.barebonescms.com/"); 380 | if (!$result["success"]) echo "[FAIL] An error occurred. " . $result["error"] . "\n"; 381 | else if ($result["response"]["code"] != 200) echo "[FAIL] An unexpected response code was returned. " . $result["response"]["line"] . "\n"; 382 | else 383 | { 384 | echo "[PASS] The expected response was returned.\n"; 385 | 386 | $html->load($result["body"]); 387 | $rows = $html->find('a[href]'); 388 | foreach ($rows as $row) 389 | { 390 | echo "\t" . HTTP::ConvertRelativeToAbsoluteURL($result["url"], $row->href) . "\n"; 391 | } 392 | } 393 | 394 | // Test asynchronous access. 395 | $urls = array( 396 | "http://www.barebonescms.com/", 397 | "http://www.cubiclesoft.com/", 398 | "http://\xF0\x9F\x98\x80.cubiclesoft.com/" 399 | ); 400 | 401 | // Build the queue. 402 | $helper = new MultiAsyncHelper(); 403 | $pages = array(); 404 | foreach ($urls as $url) 405 | { 406 | $pages[$url] = new WebBrowser(); 407 | $pages[$url]->ProcessAsync($helper, $url, NULL, $url, array("debug" => true)); 408 | } 409 | 410 | // Mix in another file handle type for fun. 411 | $fp = fopen(__FILE__, "rb"); 412 | stream_set_blocking($fp, 0); 413 | $helper->Set("__fp", $fp, "MultiAsyncHelper::ReadOnly"); 414 | 415 | // Run the main loop. 416 | $result = $helper->Wait(2); 417 | while ($result["success"]) 418 | { 419 | // Process the file handle if it is ready for reading. 420 | if (isset($result["read"]["__fp"])) 421 | { 422 | $fp = $result["read"]["__fp"]; 423 | $data = fread($fp, 500); 424 | if ($data === false || feof($fp)) 425 | { 426 | echo "[PASS] File read in successfully.\n"; 427 | 428 | $helper->Remove("__fp"); 429 | } 430 | } 431 | 432 | // Process everything else. 433 | foreach ($result["removed"] as $key => $info) 434 | { 435 | if ($key === "__fp") continue; 436 | 437 | if (!$info["result"]["success"]) echo "[FAIL] Error retrieving URL (" . $key . "). " . $info["result"]["error"] . "\n"; 438 | else if ($info["result"]["response"]["code"] != 200) echo "[FAIL] Error retrieving URL (" . $key . "). Server returned: " . $info["result"]["response"]["line"] . "\n"; 439 | else 440 | { 441 | echo "[PASS] The expected response was returned (" . $key . "). " . strlen($info["result"]["body"]) . " bytes returned.\n"; 442 | } 443 | 444 | unset($pages[$key]); 445 | } 446 | 447 | 448 | // Break out of the loop when nothing is left. 449 | if ($result["numleft"] < 1) break; 450 | 451 | $result = $helper->Wait(2); 452 | } 453 | 454 | // An error occurred. 455 | if (!$result["success"]) echo "[FAIL] Error in Wait(). " . $result["error"] . "\n"; 456 | ?> -------------------------------------------------------------------------------- /tests/test_web_server.php: -------------------------------------------------------------------------------- 1 | Start("127.0.0.1", "5578"); 24 | if (!$result["success"]) 25 | { 26 | var_dump($result); 27 | exit(); 28 | } 29 | 30 | echo "Ready.\n"; 31 | 32 | function ProcessAPI($url, $data) 33 | { 34 | if (!is_array($data)) return array("success" => false, "error" => "Data sent is not an array/object or was not able to be decoded.", "errorcode" => "invalid_data"); 35 | 36 | if (!isset($data["pre"]) || !isset($data["op"]) || !isset($data["post"])) return array("success" => false, "error" => "Missing required 'pre', 'op', or 'post' data entries.", "errorcode" => "missing_data"); 37 | 38 | // Sanitize inputs. 39 | $data["pre"] = (double)$data["pre"]; 40 | $data["op"] = (string)$data["op"]; 41 | $data["post"] = (double)$data["post"]; 42 | 43 | $question = $data["pre"] . " " . $data["op"] . " " . $data["post"]; 44 | if ($data["op"] === "+") $answer = $data["pre"] + $data["post"]; 45 | else if ($data["op"] === "-") $answer = $data["pre"] - $data["post"]; 46 | else if ($data["op"] === "*") $answer = $data["pre"] * $data["post"]; 47 | else if ($data["op"] === "/" && $data["post"] != 0) $answer = $data["pre"] / $data["post"]; 48 | else $answer = "NaN"; 49 | 50 | $result = array( 51 | "success" => true, 52 | "response" => array( 53 | "question" => $question, 54 | "answer" => $answer 55 | ) 56 | ); 57 | 58 | return $result; 59 | } 60 | 61 | do 62 | { 63 | // Implement the stream_select() call directly since multiple server instances are involved. 64 | $timeout = 30; 65 | $readfps = array(); 66 | $writefps = array(); 67 | $exceptfps = NULL; 68 | $webserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 69 | $wsserver->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 70 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 71 | if ($result === false) break; 72 | 73 | // Web server. 74 | $result = $webserver->Wait(0); 75 | 76 | // Do something with active clients. 77 | foreach ($result["clients"] as $id => $client) 78 | { 79 | if ($client->appdata === false) 80 | { 81 | echo "Client ID " . $id . " connected.\n"; 82 | 83 | $client->appdata = array("validapikey" => false); 84 | } 85 | 86 | // Example of checking for an API key. 87 | if (!$client->appdata["validapikey"] && isset($client->requestvars["apikey"]) && $client->requestvars["apikey"] === "123456789101112") 88 | { 89 | echo "Valid API key used.\n"; 90 | 91 | // Guaranteed to have at least the request line and headers if the request is incomplete. 92 | // Raise the upload limit to ~10MB for requests that haven't completed yet. Just an example. 93 | if (!$client->requestcomplete) 94 | { 95 | $options = $client->GetHTTPOptions(); 96 | $options["recvlimit"] = 10000000; 97 | $client->SetHTTPOptions($options); 98 | } 99 | 100 | $client->appdata["validapikey"] = true; 101 | } 102 | 103 | // Wait until the request is complete before fully processing inputs. 104 | if ($client->requestcomplete) 105 | { 106 | if (!$client->appdata["validapikey"]) 107 | { 108 | echo "Missing API key.\n"; 109 | 110 | $client->SetResponseCode(403); 111 | $client->SetResponseContentType("application/json"); 112 | $client->AddResponseContent(json_encode(array("success" => false, "error" => "Invalid or missing 'apikey'.", "errorcode" => "invalid_missing_apikey"))); 113 | $client->FinalizeResponse(); 114 | } 115 | else 116 | { 117 | // Handle WebSocket upgrade requests. 118 | $id2 = $wsserver->ProcessWebServerClientUpgrade($webserver, $client); 119 | if ($id2 !== false) 120 | { 121 | echo "Client ID " . $id . " upgraded to WebSocket. WebSocket client ID is " . $id2 . ".\n"; 122 | } 123 | else 124 | { 125 | echo "Sending API response.\n"; 126 | 127 | // Attempt to normalize input. 128 | if ($client->contenthandled) $data = $client->requestvars; 129 | else if (!is_object($client->readdata)) $data = @json_decode($client->readdata, true); 130 | else 131 | { 132 | $client->readdata->Open(); 133 | $data = @json_decode($client->readdata->Read(1000000), true); 134 | } 135 | 136 | // Process the request. 137 | $result2 = ProcessAPI($client->url, $data); 138 | if (!$result2["success"]) $client->SetResponseCode(400); 139 | 140 | // Send the response. 141 | $client->SetResponseContentType("application/json"); 142 | $client->AddResponseContent(json_encode($result2)); 143 | $client->FinalizeResponse(); 144 | } 145 | } 146 | } 147 | } 148 | 149 | // Do something with removed clients. 150 | foreach ($result["removed"] as $id => $result2) 151 | { 152 | if ($result2["client"]->appdata !== false) 153 | { 154 | echo "Client ID " . $id . " disconnected.\n"; 155 | 156 | // echo "Client ID " . $id . " disconnected. Reason:\n"; 157 | // var_dump($result2["result"]); 158 | // echo "\n"; 159 | } 160 | } 161 | 162 | // WebSocket server. 163 | $result = $wsserver->Wait(0); 164 | 165 | // Do something with active clients. 166 | foreach ($result["clients"] as $id => $client) 167 | { 168 | // This example processes the client input (add/multiply two numbers together) and sends back the result. 169 | $ws = $client->websocket; 170 | 171 | $result2 = $ws->Read(); 172 | while ($result2["success"] && $result2["data"] !== false) 173 | { 174 | echo "Sending API response via WebSocket.\n"; 175 | 176 | // Attempt to normalize the input. 177 | $data = @json_decode($result2["data"]["payload"], true); 178 | 179 | // Process the request. 180 | $result3 = ProcessAPI($client->url, $data); 181 | 182 | // Send the response. 183 | $result2 = $ws->Write(json_encode($result3), $result2["data"]["opcode"]); 184 | 185 | $result2 = $ws->Read(); 186 | } 187 | } 188 | 189 | // Do something with removed clients. 190 | foreach ($result["removed"] as $id => $result2) 191 | { 192 | if ($result2["client"]->appdata !== false) 193 | { 194 | echo "WebSocket client ID " . $id . " disconnected.\n"; 195 | 196 | // echo "WebSocket client ID " . $id . " disconnected. Reason:\n"; 197 | // var_dump($result2["result"]); 198 | // echo "\n"; 199 | } 200 | } 201 | } while (1); 202 | ?> -------------------------------------------------------------------------------- /tests/test_websocket_client.php: -------------------------------------------------------------------------------- 1 | Connect("ws://127.0.0.1:5578/math?apikey=123456789101112", "http://localhost"); 19 | if (!$result["success"]) 20 | { 21 | var_dump($result); 22 | exit(); 23 | } 24 | 25 | // Send a text frame (just an example). 26 | // Get the answer to 5 + 10. 27 | $data = array( 28 | "pre" => "5", 29 | "op" => "+", 30 | "post" => "10", 31 | ); 32 | $result = $ws->Write(json_encode($data), WebSocket::FRAMETYPE_TEXT); 33 | 34 | // Send a binary frame in two fragments (just an example). 35 | // Get the answer to 5 * 10. 36 | $data["op"] = "*"; 37 | $data2 = json_encode($data); 38 | $y = (int)(strlen($data2) / 2); 39 | $result = $ws->Write(substr($data2, 0, $y), WebSocket::FRAMETYPE_BINARY, false); 40 | $result = $ws->Write(substr($data2, $y), WebSocket::FRAMETYPE_BINARY); 41 | 42 | // Main loop. 43 | $result = $ws->Wait(); 44 | while ($result["success"]) 45 | { 46 | do 47 | { 48 | $result = $ws->Read(); 49 | if (!$result["success"]) break; 50 | if ($result["data"] !== false) 51 | { 52 | // Do something with the data. 53 | echo "Raw message from server:\n"; 54 | var_dump($result["data"]); 55 | echo "\n"; 56 | 57 | $data = json_decode($result["data"]["payload"], true); 58 | echo "The server said: " . ($data["success"] ? $data["response"]["question"] . " = " . $data["response"]["answer"] : $data["error"] . " (" . $data["errorcode"] . ")") . "\n\n"; 59 | } 60 | } while ($result["data"] !== false); 61 | 62 | $result = $ws->Wait(); 63 | } 64 | 65 | // An error occurred. 66 | var_dump($result); 67 | ?> -------------------------------------------------------------------------------- /tests/test_websocket_server.php: -------------------------------------------------------------------------------- 1 | Start("127.0.0.1", "5578"); 23 | if (!$result["success"]) 24 | { 25 | var_dump($result); 26 | exit(); 27 | } 28 | 29 | echo "Ready.\n"; 30 | 31 | function ProcessAPI($url, $data) 32 | { 33 | if (!is_array($data)) return array("success" => false, "error" => "Data sent is not an array/object or was not able to be decoded.", "errorcode" => "invalid_data"); 34 | 35 | if (!isset($data["pre"]) || !isset($data["op"]) || !isset($data["post"])) return array("success" => false, "error" => "Missing required 'pre', 'op', or 'post' data entries.", "errorcode" => "missing_data"); 36 | 37 | // Sanitize inputs. 38 | $data["pre"] = (double)$data["pre"]; 39 | $data["op"] = (string)$data["op"]; 40 | $data["post"] = (double)$data["post"]; 41 | 42 | $question = $data["pre"] . " " . $data["op"] . " " . $data["post"]; 43 | if ($data["op"] === "+") $answer = $data["pre"] + $data["post"]; 44 | else if ($data["op"] === "-") $answer = $data["pre"] - $data["post"]; 45 | else if ($data["op"] === "*") $answer = $data["pre"] * $data["post"]; 46 | else if ($data["op"] === "/" && $data["post"] != 0) $answer = $data["pre"] / $data["post"]; 47 | else $answer = "NaN"; 48 | 49 | $result = array( 50 | "success" => true, 51 | "response" => array( 52 | "question" => $question, 53 | "answer" => $answer 54 | ) 55 | ); 56 | 57 | return $result; 58 | } 59 | 60 | do 61 | { 62 | $result = $wsserver->Wait(); 63 | 64 | // Do something with active clients. 65 | foreach ($result["clients"] as $id => $client) 66 | { 67 | if ($client->appdata === false) 68 | { 69 | echo "Client ID " . $id . " connected.\n"; 70 | 71 | // Example of checking for an API key. 72 | $url = HTTP::ExtractURL($client->url); 73 | if (!isset($url["queryvars"]["apikey"]) || $url["queryvars"]["apikey"][0] !== "123456789101112") 74 | { 75 | $wsserver->RemoveClient($id); 76 | 77 | continue; 78 | } 79 | 80 | echo "Valid API key used.\n"; 81 | 82 | $client->appdata = array(); 83 | } 84 | 85 | // This example processes the client input (add/multiply two numbers together) and sends back the result. 86 | $ws = $client->websocket; 87 | 88 | $result2 = $ws->Read(); 89 | while ($result2["success"] && $result2["data"] !== false) 90 | { 91 | echo "Sending API response via WebSocket.\n"; 92 | 93 | // Attempt to normalize the input. 94 | $data = json_decode($result2["data"]["payload"], true); 95 | 96 | // Process the request. 97 | $result3 = ProcessAPI($client->url, $data); 98 | 99 | // Send the response. 100 | $result2 = $ws->Write(json_encode($result3), $result2["data"]["opcode"]); 101 | 102 | $result2 = $ws->Read(); 103 | } 104 | } 105 | 106 | // Do something with removed clients. 107 | foreach ($result["removed"] as $id => $result2) 108 | { 109 | if ($result2["client"]->appdata !== false) 110 | { 111 | echo "Client ID " . $id . " disconnected.\n"; 112 | 113 | // echo "Client ID " . $id . " disconnected. Reason:\n"; 114 | // var_dump($result2["result"]); 115 | // echo "\n"; 116 | } 117 | } 118 | } while (1); 119 | ?> -------------------------------------------------------------------------------- /tests/test_xss.txt: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Plain text 23 | 24 | Test 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | xxs link 33 | xxs link 34 | "> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | < 52 | 54 | 55 | 56 | 57 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 |
79 |
80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | < 96 | ScRiPt SrC = "xss.jpg" 97 | 98 | "" 99 | >< 100 | / 101 | 102 | sCrIpT 103 | /bogus 104 | > 105 | 106 | 107 | 108 | "> 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | PT SRC="xss.js"> 117 | 118 | < fakevoid onload="return 119 | confirm('Wanna give me your password?');" / 120 | > 121 | '); 124 | document.write('

But include me!

'); 125 | //document.write('< / 126 | script bogus 127 | > 128 |

Also include me!

129 | -------------------------------------------------------------------------------- /websocket_server.php: -------------------------------------------------------------------------------- 1 | Reset(); 18 | } 19 | 20 | public function Reset() 21 | { 22 | if (!class_exists("WebSocket", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/websocket.php"; 23 | 24 | $this->fp = false; 25 | $this->clients = array(); 26 | $this->nextclientid = 1; 27 | $this->websocketclass = "WebSocket"; 28 | $this->origins = false; 29 | 30 | $this->defaultclosemode = WebSocket::CLOSE_IMMEDIATELY; 31 | $this->defaultmaxreadframesize = 2000000; 32 | $this->defaultmaxreadmessagesize = 10000000; 33 | $this->defaultkeepalive = 30; 34 | $this->lasttimeoutcheck = time(); 35 | } 36 | 37 | public function __destruct() 38 | { 39 | $this->Stop(); 40 | } 41 | 42 | public function SetWebSocketClass($newclass) 43 | { 44 | if (class_exists($newclass)) $this->websocketclass = $newclass; 45 | } 46 | 47 | public function SetAllowedOrigins($origins) 48 | { 49 | if (is_string($origins)) $origins = array($origins); 50 | if (!is_array($origins)) $origins = false; 51 | else if (isset($origins[0])) $origins = array_flip($origins); 52 | 53 | $this->origins = $origins; 54 | } 55 | 56 | public function SetDefaultCloseMode($mode) 57 | { 58 | $this->defaultclosemode = $mode; 59 | } 60 | 61 | public function SetDefaultKeepAliveTimeout($keepalive) 62 | { 63 | $this->defaultkeepalive = (int)$keepalive; 64 | } 65 | 66 | public function SetDefaultMaxReadFrameSize($maxsize) 67 | { 68 | $this->defaultmaxreadframesize = (is_bool($maxsize) ? false : (int)$maxsize); 69 | } 70 | 71 | public function SetDefaultMaxReadMessageSize($maxsize) 72 | { 73 | $this->defaultmaxreadmessagesize = (is_bool($maxsize) ? false : (int)$maxsize); 74 | } 75 | 76 | // Starts the server on the host and port. 77 | // $host is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6. 78 | public function Start($host, $port) 79 | { 80 | $this->Stop(); 81 | 82 | $this->fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr); 83 | if ($this->fp === false) return array("success" => false, "error" => self::WSTranslate("Bind() failed. Reason: %s (%d)", $errorstr, $errornum), "errorcode" => "bind_failed"); 84 | 85 | // Enable non-blocking mode. 86 | stream_set_blocking($this->fp, 0); 87 | 88 | return array("success" => true); 89 | } 90 | 91 | public function Stop() 92 | { 93 | foreach ($this->clients as $client) 94 | { 95 | if ($client->websocket !== false) $client->websocket->Disconnect(); 96 | else fclose($client->fp); 97 | } 98 | 99 | $this->clients = array(); 100 | 101 | if ($this->fp !== false) 102 | { 103 | fclose($this->fp); 104 | 105 | $this->fp = false; 106 | } 107 | 108 | $this->nextclientid = 1; 109 | } 110 | 111 | // Dangerous but allows for stream_select() calls on multiple, separate stream handles. 112 | public function GetStream() 113 | { 114 | return $this->fp; 115 | } 116 | 117 | // Return whatever response/headers are needed here. 118 | protected function ProcessNewConnection($method, $path, $client) 119 | { 120 | $result = ""; 121 | 122 | if ($method !== "GET") $result .= "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n"; 123 | else if (!isset($client->headers["Host"]) || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false || !isset($client->headers["Sec-Websocket-Key"])) 124 | { 125 | $result .= "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"; 126 | } 127 | else if (!isset($client->headers["Sec-Websocket-Version"]) || $client->headers["Sec-Websocket-Version"] != 13) 128 | { 129 | $result .= "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nConnection: close\r\n\r\n"; 130 | } 131 | else if (!isset($client->headers["Origin"]) || ($this->origins !== false && !isset($this->origins[strtolower($client->headers["Origin"])]))) 132 | { 133 | $result .= "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n"; 134 | } 135 | 136 | return $result; 137 | } 138 | 139 | // Return whatever additional HTTP headers are needed here. 140 | protected function ProcessAcceptedConnection($method, $path, $client) 141 | { 142 | return ""; 143 | } 144 | 145 | protected function InitNewClient($fp) 146 | { 147 | $client = new stdClass(); 148 | 149 | $client->id = $this->nextclientid; 150 | $client->readdata = ""; 151 | $client->writedata = ""; 152 | $client->request = false; 153 | $client->path = ""; 154 | $client->url = ""; 155 | $client->headers = array(); 156 | $client->lastheader = ""; 157 | $client->websocket = false; 158 | $client->fp = $fp; 159 | $client->ipaddr = stream_socket_get_name($fp, true); 160 | 161 | // Intended for application storage. 162 | $client->appdata = false; 163 | 164 | $this->clients[$this->nextclientid] = $client; 165 | 166 | $this->nextclientid++; 167 | 168 | return $client; 169 | } 170 | 171 | private function ProcessInitialResponse($method, $path, $client) 172 | { 173 | // Let a derived class handle the new connection (e.g. processing Origin and Host). 174 | // Since the 'websocketclass' is instantiated AFTER this function, it is possible to switch classes on the fly. 175 | $client->writedata .= $this->ProcessNewConnection($method, $path, $client); 176 | 177 | // If an error occurs, the connection will still terminate. 178 | $client->websocket = new $this->websocketclass(); 179 | $client->websocket->SetCloseMode($this->defaultclosemode); 180 | $client->websocket->SetKeepAliveTimeout($this->defaultkeepalive); 181 | 182 | // If nothing was output, accept the connection. 183 | if ($client->writedata === "") 184 | { 185 | $client->writedata .= "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"; 186 | $client->writedata .= "Sec-WebSocket-Accept: " . base64_encode(sha1($client->headers["Sec-Websocket-Key"] . WebSocket::KEY_GUID, true)) . "\r\n"; 187 | $client->writedata .= $this->ProcessAcceptedConnection($method, $path, $client); 188 | $client->writedata .= "\r\n"; 189 | 190 | // Finish class initialization. 191 | $client->websocket->SetServerMode(); 192 | $client->websocket->SetMaxReadFrameSize($this->defaultmaxreadframesize); 193 | $client->websocket->SetMaxReadMessageSize($this->defaultmaxreadmessagesize); 194 | 195 | // Set the socket in the WebSocket class. 196 | $client->websocket->Connect("", "", array("connected_fp" => $client->fp)); 197 | } 198 | 199 | $this->UpdateClientState($client->id); 200 | } 201 | 202 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 203 | { 204 | if ($this->fp !== false) $readfps[$prefix . "ws_s"] = $this->fp; 205 | if ($timeout === false || $timeout > $this->defaultkeepalive) $timeout = $this->defaultkeepalive; 206 | 207 | foreach ($this->clients as $id => $client) 208 | { 209 | if ($client->writedata === "") $readfps[$prefix . "ws_c_" . $id] = $client->fp; 210 | 211 | if ($client->writedata !== "" || ($client->websocket !== false && $client->websocket->NeedsWrite())) $writefps[$prefix . "ws_c_" . $id] = $client->fp; 212 | 213 | if ($client->websocket !== false) 214 | { 215 | $timeout2 = $client->websocket->GetKeepAliveTimeout(); 216 | if ($timeout > $timeout2) $timeout = $timeout2; 217 | } 218 | } 219 | } 220 | 221 | // Sometimes keyed arrays don't work properly. 222 | public static function FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 223 | { 224 | // In order to correctly detect bad outputs, no '0' integer key is allowed. 225 | if (isset($readfps[0]) || isset($writefps[0]) || ($exceptfps !== NULL && isset($exceptfps[0]))) return false; 226 | 227 | $origreadfps = $readfps; 228 | $origwritefps = $writefps; 229 | $origexceptfps = $exceptfps; 230 | 231 | $result2 = @stream_select($readfps, $writefps, $exceptfps, $timeout); 232 | if ($result2 === false) return false; 233 | 234 | if (isset($readfps[0])) 235 | { 236 | $fps = array(); 237 | foreach ($origreadfps as $key => $fp) $fps[(int)$fp] = $key; 238 | 239 | foreach ($readfps as $num => $fp) 240 | { 241 | $readfps[$fps[(int)$fp]] = $fp; 242 | 243 | unset($readfps[$num]); 244 | } 245 | } 246 | 247 | if (isset($writefps[0])) 248 | { 249 | $fps = array(); 250 | foreach ($origwritefps as $key => $fp) $fps[(int)$fp] = $key; 251 | 252 | foreach ($writefps as $num => $fp) 253 | { 254 | $writefps[$fps[(int)$fp]] = $fp; 255 | 256 | unset($writefps[$num]); 257 | } 258 | } 259 | 260 | if ($exceptfps !== NULL && isset($exceptfps[0])) 261 | { 262 | $fps = array(); 263 | foreach ($origexceptfps as $key => $fp) $fps[(int)$fp] = $key; 264 | 265 | foreach ($exceptfps as $num => $fp) 266 | { 267 | $exceptfps[$fps[(int)$fp]] = $fp; 268 | 269 | unset($exceptfps[$num]); 270 | } 271 | } 272 | 273 | return true; 274 | } 275 | 276 | // Handles new connections, the initial conversation, basic packet management, and timeouts. 277 | // Can wait on more streams than just sockets and/or more sockets. Useful for waiting on other resources. 278 | // 'ws_s' and the 'ws_c_' prefix are reserved. 279 | // Returns an array of clients that may need more processing. 280 | public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL) 281 | { 282 | $this->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 283 | 284 | $result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array(), "accepted" => array(), "read" => array(), "write" => array()); 285 | if (!count($readfps) && !count($writefps)) return $result; 286 | 287 | $result2 = self::FixedStreamSelect($readfps, $writefps, $exceptfps, $timeout); 288 | if ($result2 === false) return array("success" => false, "error" => self::WSTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 289 | 290 | // Return handles that were being waited on. 291 | $result["readfps"] = $readfps; 292 | $result["writefps"] = $writefps; 293 | $result["exceptfps"] = $exceptfps; 294 | 295 | $this->ProcessWaitResult($result); 296 | 297 | return $result; 298 | } 299 | 300 | protected function ProcessWaitResult(&$result) 301 | { 302 | // Handle new connections. 303 | if (isset($result["readfps"]["ws_s"])) 304 | { 305 | while (($fp = @stream_socket_accept($this->fp, 0)) !== false) 306 | { 307 | // Enable non-blocking mode. 308 | stream_set_blocking($fp, 0); 309 | 310 | $client = $this->InitNewClient($fp); 311 | 312 | $result["accepted"][$client->id] = $client; 313 | } 314 | 315 | unset($result["readfps"]["ws_s"]); 316 | } 317 | 318 | // Handle clients in the read queue. 319 | foreach ($result["readfps"] as $cid => $fp) 320 | { 321 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue; 322 | 323 | $id = (int)substr($cid, 5); 324 | 325 | if (!isset($this->clients[$id])) continue; 326 | 327 | $client = $this->clients[$id]; 328 | 329 | $result["read"][$id] = $client; 330 | 331 | if ($client->websocket !== false) 332 | { 333 | $this->ProcessClientQueuesAndTimeoutState($result, $id, true, isset($result["writefps"][$cid])); 334 | 335 | // Remove active WebSocket clients from the write queue. 336 | unset($result["writefps"][$cid]); 337 | } 338 | else 339 | { 340 | $result2 = @fread($fp, 8192); 341 | if ($result2 === false || ($result2 === "" && feof($fp))) 342 | { 343 | @fclose($fp); 344 | 345 | unset($this->clients[$id]); 346 | } 347 | else 348 | { 349 | $client->readdata .= $result2; 350 | 351 | if (strlen($client->readdata) > 100000) 352 | { 353 | // Bad header size. Just kill the connection. 354 | @fclose($fp); 355 | 356 | unset($this->clients[$id]); 357 | } 358 | else 359 | { 360 | while (($pos = strpos($client->readdata, "\n")) !== false) 361 | { 362 | // Retrieve the next line of input. 363 | $line = rtrim(substr($client->readdata, 0, $pos)); 364 | $client->readdata = (string)substr($client->readdata, $pos + 1); 365 | 366 | if ($client->request === false) $client->request = trim($line); 367 | else if ($line !== "") 368 | { 369 | // Process the header. 370 | if ($client->lastheader != "" && (substr($line, 0, 1) == " " || substr($line, 0, 1) == "\t")) $client->headers[$client->lastheader] .= $header; 371 | else 372 | { 373 | $pos = strpos($line, ":"); 374 | if ($pos === false) $pos = strlen($line); 375 | $client->lastheader = self::HeaderNameCleanup(substr($line, 0, $pos)); 376 | $client->headers[$client->lastheader] = ltrim(substr($line, $pos + 1)); 377 | } 378 | } 379 | else 380 | { 381 | // Headers have all been received. Process the client request. 382 | $request = $client->request; 383 | $pos = strpos($request, " "); 384 | if ($pos === false) $pos = strlen($request); 385 | $method = (string)substr($request, 0, $pos); 386 | $request = trim(substr($request, $pos)); 387 | 388 | $pos = strrpos($request, " "); 389 | if ($pos === false) $pos = strlen($request); 390 | $path = (string)substr($request, 0, $pos); 391 | if ($path === "") $path = "/"; 392 | 393 | if (isset($client->headers["Host"])) $client->headers["Host"] = preg_replace('/[^a-z0-9.:\[\]_-]/', "", strtolower($client->headers["Host"])); 394 | 395 | $client->path = $path; 396 | $client->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 397 | 398 | $this->ProcessInitialResponse($method, $path, $client); 399 | 400 | break; 401 | } 402 | } 403 | } 404 | } 405 | } 406 | 407 | unset($result["readfps"][$cid]); 408 | } 409 | 410 | // Handle remaining clients in the write queue. 411 | foreach ($result["writefps"] as $cid => $fp) 412 | { 413 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "ws_c_") continue; 414 | 415 | $id = (int)substr($cid, 5); 416 | 417 | if (!isset($this->clients[$id])) continue; 418 | 419 | $client = $this->clients[$id]; 420 | 421 | $result["write"][$id] = $client; 422 | 423 | if ($client->writedata === "") $this->ProcessClientQueuesAndTimeoutState($result, $id, false, true); 424 | else 425 | { 426 | $result2 = @fwrite($fp, $client->writedata); 427 | if ($result2 === false || ($result2 === "" && feof($fp))) 428 | { 429 | @fclose($fp); 430 | 431 | unset($this->clients[$id]); 432 | } 433 | else if ($result2 === 0) $this->ProcessClientQueuesAndTimeoutState($result, $id, true, false, 1); 434 | else 435 | { 436 | $client->writedata = (string)substr($client->writedata, $result2); 437 | 438 | // Let the application know about the new client or close the connection if the WebSocket Upgrade request failed. 439 | if ($client->writedata === "") 440 | { 441 | if ($client->websocket->GetStream() !== false) $result["clients"][$id] = $client; 442 | else 443 | { 444 | @fclose($fp); 445 | 446 | unset($this->clients[$id]); 447 | } 448 | } 449 | } 450 | } 451 | 452 | unset($result["writefps"][$cid]); 453 | } 454 | 455 | // Handle client timeouts. 456 | $ts = time(); 457 | if ($this->lasttimeoutcheck <= $ts - 5) 458 | { 459 | foreach ($this->clients as $id => $client) 460 | { 461 | if (!isset($result["clients"][$id]) && $client->writedata === "" && $client->websocket !== false) 462 | { 463 | $this->ProcessClientQueuesAndTimeoutState($result, $id, false, false); 464 | } 465 | } 466 | 467 | $this->lasttimeoutcheck = $ts; 468 | } 469 | } 470 | 471 | protected function ProcessClientQueuesAndTimeoutState(&$result, $id, $read, $write, $readsize = 65536) 472 | { 473 | $client = $this->clients[$id]; 474 | 475 | $result2 = $client->websocket->ProcessQueuesAndTimeoutState($read, $write, $readsize); 476 | if ($result2["success"]) $result["clients"][$id] = $client; 477 | else 478 | { 479 | $result["removed"][$id] = array("result" => $result2, "client" => $client); 480 | 481 | $this->RemoveClient($id); 482 | } 483 | } 484 | 485 | public function GetClients() 486 | { 487 | return $this->clients; 488 | } 489 | 490 | public function NumClients() 491 | { 492 | return count($this->clients); 493 | } 494 | 495 | public function UpdateClientState($id) 496 | { 497 | } 498 | 499 | public function GetClient($id) 500 | { 501 | return (isset($this->clients[$id]) ? $this->clients[$id] : false); 502 | } 503 | 504 | public function RemoveClient($id) 505 | { 506 | if (isset($this->clients[$id])) 507 | { 508 | $client = $this->clients[$id]; 509 | 510 | // Remove the client. 511 | if ($client->websocket->GetStream() !== false) 512 | { 513 | $client->websocket->Disconnect(); 514 | $client->websocket = false; 515 | $client->fp = false; 516 | } 517 | 518 | if ($client->fp !== false) @fclose($client->fp); 519 | 520 | unset($this->clients[$id]); 521 | } 522 | } 523 | 524 | public function ProcessWebServerClientUpgrade($webserver, $client) 525 | { 526 | if (!($client instanceof WebServer_Client)) return false; 527 | 528 | if (!$client->requestcomplete || $client->mode === "handle_response") return false; 529 | if ($client->request["method"] !== "GET" || !isset($client->headers["Connection"]) || stripos($client->headers["Connection"], "upgrade") === false || !isset($client->headers["Upgrade"]) || stripos($client->headers["Upgrade"], "websocket") === false) return false; 530 | 531 | // Create an equivalent WebSocket server client class. 532 | $webserver->DetachClient($client->id); 533 | 534 | $method = $client->request["method"]; 535 | $path = $client->request["path"]; 536 | 537 | $client2 = $this->InitNewClient($client->fp); 538 | $client2->request = $client->request["line"]; 539 | $client2->headers = $client->headers; 540 | $client2->path = $path; 541 | $client2->url = "ws://" . (isset($client->headers["Host"]) ? $client->headers["Host"] : "localhost") . $path; 542 | 543 | $client2->appdata = $client->appdata; 544 | 545 | $this->ProcessInitialResponse($method, $path, $client2); 546 | 547 | return $client2->id; 548 | } 549 | 550 | public static function HeaderNameCleanup($name) 551 | { 552 | return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name))))); 553 | } 554 | 555 | public static function WSTranslate() 556 | { 557 | $args = func_get_args(); 558 | if (!count($args)) return ""; 559 | 560 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 561 | } 562 | } 563 | ?> -------------------------------------------------------------------------------- /websocket_server_libev.php: -------------------------------------------------------------------------------- 1 | ev_watchers = array(); 24 | } 25 | 26 | public function Internal_LibEvHandleEvent($watcher, $revents) 27 | { 28 | if (($revents & Ev::READ) || ($revents & Ev::ERROR)) $this->ev_read_ready[$watcher->data] = $watcher->fd; 29 | if ($revents & Ev::WRITE) $this->ev_write_ready[$watcher->data] = $watcher->fd; 30 | } 31 | 32 | public function Start($host, $port) 33 | { 34 | $result = parent::Start($host, $port); 35 | if (!$result["success"]) return $result; 36 | 37 | $this->ev_watchers["ws_s"] = new EvIo($this->fp, Ev::READ, array($this, "Internal_LibEvHandleEvent"), "ws_s"); 38 | 39 | return $result; 40 | } 41 | 42 | public function Stop() 43 | { 44 | parent::Stop(); 45 | 46 | foreach ($this->ev_watchers as $key => $watcher) 47 | { 48 | $watcher->stop(); 49 | } 50 | 51 | $this->ev_watchers = array(); 52 | } 53 | 54 | protected function InitNewClient($fp) 55 | { 56 | $client = parent::InitNewClient($fp); 57 | 58 | $this->ev_watchers["ws_c_" . $client->id] = new EvIo($client->fp, Ev::READ, array($this, "Internal_LibEvHandleEvent"), "ws_c_" . $client->id); 59 | 60 | return $client; 61 | } 62 | 63 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 64 | { 65 | if ($this->fp !== false) $readfps[$prefix . "ws_s"] = $this->fp; 66 | if ($timeout === false || $timeout > $this->defaultkeepalive) $timeout = $this->defaultkeepalive; 67 | 68 | foreach ($this->clients as $id => $client) 69 | { 70 | if ($client->writedata === "") $readfps[$prefix . "ws_c_" . $id] = $client->fp; 71 | 72 | if ($client->writedata !== "" || ($client->websocket !== false && $client->websocket->NeedsWrite())) $writefps[$prefix . "ws_c_" . $id] = $client->fp; 73 | } 74 | } 75 | 76 | public function Internal_LibEvTimeout($watcher, $revents) 77 | { 78 | Ev::stop(Ev::BREAK_ALL); 79 | } 80 | 81 | public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL) 82 | { 83 | if ($timeout === false || $timeout > $this->defaultkeepalive) $timeout = $this->defaultkeepalive; 84 | 85 | $result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array(), "accepted" => array(), "read" => array(), "write" => array()); 86 | if (!count($this->ev_watchers) && !count($readfps) && !count($writefps)) return $result; 87 | 88 | $this->ev_read_ready = array(); 89 | $this->ev_write_ready = array(); 90 | 91 | // Temporarily attach other read/write handles. 92 | $tempwatchers = array(); 93 | 94 | foreach ($readfps as $key => $fp) 95 | { 96 | $tempwatchers[] = new EvIo($fp, Ev::READ, array($this, "Internal_LibEvHandleEvent"), $key); 97 | } 98 | 99 | foreach ($writefps as $key => $fp) 100 | { 101 | $tempwatchers[] = new EvIo($fp, Ev::WRITE, array($this, "Internal_LibEvHandleEvent"), $key); 102 | } 103 | 104 | $tempwatchers[] = new EvTimer($timeout, 0, array($this, "Internal_LibEvTimeout")); 105 | 106 | // Wait for one or more events to fire. 107 | Ev::run(Ev::RUN_ONCE); 108 | 109 | // Remove temporary watchers. 110 | foreach ($tempwatchers as $watcher) $watcher->stop(); 111 | 112 | // Return handles that were being waited on. 113 | $result["readfps"] = $this->ev_read_ready; 114 | $result["writefps"] = $this->ev_write_ready; 115 | $result["exceptfps"] = (is_array($exceptfps) ? array() : $exceptfps); 116 | 117 | $this->ProcessWaitResult($result); 118 | 119 | // Post-process clients. 120 | foreach ($result["clients"] as $id => $client) 121 | { 122 | $this->UpdateClientState($id); 123 | } 124 | 125 | return $result; 126 | } 127 | 128 | public function UpdateClientState($id) 129 | { 130 | if (isset($this->clients[$id])) 131 | { 132 | $client = $this->clients[$id]; 133 | 134 | $events = 0; 135 | 136 | if ($client->writedata === "") $events = Ev::READ; 137 | 138 | if ($client->writedata !== "" || ($client->websocket !== false && $client->websocket->NeedsWrite())) $events |= Ev::WRITE; 139 | 140 | $this->ev_watchers["ws_c_" . $id]->set($client->fp, $events); 141 | } 142 | } 143 | 144 | public function RemoveClient($id) 145 | { 146 | parent::RemoveClient($id); 147 | 148 | if (isset($this->ev_watchers["ws_c_" . $id])) 149 | { 150 | $this->ev_watchers["ws_c_" . $id]->stop(); 151 | 152 | unset($this->ev_watchers["ws_c_" . $id]); 153 | } 154 | } 155 | } 156 | ?> --------------------------------------------------------------------------------