├── .gitignore ├── support └── createprocess.exe ├── sdk └── support │ ├── sdk_cloud_storage_server_scripts.php │ ├── sdk_remotedapi.php │ ├── crc32_stream.php │ ├── webroute.php │ ├── deflate_stream.php │ ├── sdk_cloud_storage_server_api_base.php │ ├── random.php │ ├── utf_utils.php │ ├── websocket.php │ └── web_browser.php ├── README.md └── server_exts └── scripts.php /.gitignore: -------------------------------------------------------------------------------- 1 | /*.bat 2 | /test_* 3 | /cache 4 | -------------------------------------------------------------------------------- /support/createprocess.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/cloud-storage-server-ext-scripts/master/support/createprocess.exe -------------------------------------------------------------------------------- /sdk/support/sdk_cloud_storage_server_scripts.php: -------------------------------------------------------------------------------- 1 | apiprefix = "/scripts/v1"; 16 | } 17 | 18 | public function RunScript($name, $args = array(), $stdin = "", $queue = false) 19 | { 20 | $options = array( 21 | "name" => $name, 22 | "args" => $args, 23 | "stdin" => $stdin 24 | ); 25 | 26 | if ($queue !== false) $options["queue"] = (int)$queue; 27 | 28 | return $this->RunAPI("POST", "run", $options); 29 | } 30 | 31 | public function CancelScript($id) 32 | { 33 | return $this->RunAPI("POST", "cancel/" . $id); 34 | } 35 | 36 | public function GetStatus($id = false) 37 | { 38 | return $this->RunAPI("GET", "status" . ($id !== false ? "/" . $id : "")); 39 | } 40 | 41 | public function StartMonitoring($name) 42 | { 43 | $result = $this->InitWebSocket(); 44 | if (!$result["success"]) return $result; 45 | 46 | $options = array( 47 | "api_method" => "GET", 48 | "api_path" => $this->apiprefix . "/monitor", 49 | "api_sequence" => 1, 50 | "name" => $name 51 | ); 52 | 53 | $result2 = $result["ws"]->Write(json_encode($options), WebSocket::FRAMETYPE_TEXT); 54 | if (!$result2["success"]) return $result2; 55 | 56 | $result["api_sequence"] = 1; 57 | 58 | return $result; 59 | } 60 | 61 | public function CreateGuest($name, $run, $cancel, $status, $monitor, $expires) 62 | { 63 | $options = array( 64 | "name" => $name, 65 | "run" => (int)(bool)$run, 66 | "cancel" => (int)(bool)$cancel, 67 | "status" => (int)(bool)$status, 68 | "monitor" => (int)(bool)$monitor, 69 | "expires" => (int)$expires 70 | ); 71 | 72 | return $this->RunAPI("POST", "guest/create", $options); 73 | } 74 | 75 | public function GetGuestList() 76 | { 77 | return $this->RunAPI("GET", "guest/list"); 78 | } 79 | 80 | public function DeleteGuest($id) 81 | { 82 | return $this->RunAPI("DELETE", "guest/delete/" . $id); 83 | } 84 | } 85 | ?> -------------------------------------------------------------------------------- /sdk/support/sdk_remotedapi.php: -------------------------------------------------------------------------------- 1 | false, "error" => WebRoute::WRTranslate("Invalid Remoted API URL scheme."), "errorcode" => "invalid_scheme"); 45 | 46 | $result["url"] = $url; 47 | 48 | return $result; 49 | } 50 | 51 | if ($url2["loginusername"] === "") return array("success" => false, "error" => WebRoute::WRTranslate("Remoted API URL is missing client key."), "errorcode" => "missing_client_key"); 52 | 53 | $options["headers"]["X-Remoted-APIKey"] = $url2["loginusername"]; 54 | 55 | $url2["scheme"] = ($url2["scheme"] === "rwr" ? "wr" : "wrs"); 56 | unset($url2["loginusername"]); 57 | unset($url2["login"]); 58 | 59 | $url = HTTP::CondenseURL($url2); 60 | 61 | $result = $wr->Connect($url, false, $timeout, $options, $web); 62 | if (!$result["success"]) return $result; 63 | 64 | $options["fp"] = $result["fp"]; 65 | } 66 | 67 | return $result; 68 | } 69 | } 70 | ?> -------------------------------------------------------------------------------- /sdk/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 | ?> -------------------------------------------------------------------------------- /sdk/support/webroute.php: -------------------------------------------------------------------------------- 1 | csprng = false; 17 | } 18 | 19 | public static function ProcessState($state) 20 | { 21 | while ($state->state !== "done") 22 | { 23 | switch ($state->state) 24 | { 25 | case "initialize": 26 | { 27 | $result = $state->web->Process($state->url, $state->options); 28 | if (!$result["success"]) return $result; 29 | 30 | if (isset($state->options["async"]) && $state->options["async"]) 31 | { 32 | $state->async = true; 33 | $state->webstate = $result["state"]; 34 | 35 | $state->state = "process_async"; 36 | } 37 | else 38 | { 39 | $state->result = $result; 40 | 41 | $state->state = "post_retrieval"; 42 | } 43 | 44 | break; 45 | } 46 | case "process_async": 47 | { 48 | // Run a cycle of the WebBrowser state processor. 49 | $result = $state->web->ProcessState($state->webstate); 50 | if (!$result["success"]) return $result; 51 | 52 | $state->webstate = false; 53 | $state->result = $result; 54 | 55 | $state->state = "post_retrieval"; 56 | 57 | break; 58 | } 59 | case "post_retrieval": 60 | { 61 | if ($state->result["response"]["code"] != 101) return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() failed to connect to the WebRoute. Server returned: %s %s", $result["response"]["code"], $result["response"]["meaning"]), "errorcode" => "incorrect_server_response"); 62 | if (!isset($state->result["headers"]["Sec-Webroute-Accept"])) return array("success" => false, "error" => self::WRTranslate("Server failed to include a 'Sec-WebRoute-Accept' header in its response to the request."), "errorcode" => "missing_server_webroute_accept_header"); 63 | 64 | // Verify the Sec-WebRoute-Accept response. 65 | if ($state->result["headers"]["Sec-Webroute-Accept"][0] !== base64_encode(sha1($state->options["headers"]["WebRoute-ID"] . self::ID_GUID, true))) return array("success" => false, "error" => self::WRTranslate("The server's 'Sec-WebRoute-Accept' header is invalid."), "errorcode" => "invalid_server_webroute_accept_header"); 66 | 67 | $state->state = "done"; 68 | 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return $state->result; 75 | } 76 | 77 | public function Connect($url, $id = false, $timeout = false, $options = array(), $web = false) 78 | { 79 | // Generate client ID. 80 | if ($id === false) 81 | { 82 | if (!class_exists("CSPRNG", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/random.php"; 83 | 84 | if ($this->csprng === false) $this->csprng = new CSPRNG(); 85 | 86 | $id = $this->csprng->GenerateString(64); 87 | } 88 | 89 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 90 | 91 | // Use WebBrowser to initiate the connection. 92 | if ($web === false) $web = new WebBrowser(); 93 | 94 | // Transform URL. 95 | $url2 = HTTP::ExtractURL($url); 96 | if ($url2["scheme"] != "wr" && $url2["scheme"] != "wrs") return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() only supports the 'wr' and 'wrs' protocols."), "errorcode" => "protocol_check"); 97 | $url2["scheme"] = str_replace("wr", "http", $url2["scheme"]); 98 | $url2 = HTTP::CondenseURL($url2); 99 | 100 | // Generate correct request headers. 101 | if (!isset($options["headers"])) $options["headers"] = array(); 102 | $options["headers"]["Connection"] = "keep-alive, Upgrade"; 103 | $options["headers"]["Pragma"] = "no-cache"; 104 | $options["headers"]["WebRoute-Version"] = "1"; 105 | $options["headers"]["WebRoute-ID"] = $id; 106 | if ($timeout !== false && is_int($timeout)) $options["headers"]["WebRoute-Timeout"] = (string)(int)$timeout; 107 | $options["headers"]["Upgrade"] = "webroute"; 108 | 109 | // Initialize the process state object. 110 | $state = new stdClass(); 111 | $state->async = false; 112 | $state->state = "initialize"; 113 | $state->web = $web; 114 | $state->url = $url2; 115 | $state->options = $options; 116 | $state->webstate = false; 117 | $state->result = false; 118 | 119 | // Run at least one state cycle to finish initializing the state object. 120 | $result = $this->ProcessState($state); 121 | 122 | // Return the state for async calls. Caller must call ProcessState(). 123 | if ($state->async) return array("success" => true, "id" => $id, "state" => $state); 124 | 125 | $result["id"] = $id; 126 | 127 | return $result; 128 | } 129 | 130 | // Implements the correct MultiAsyncHelper responses for WebRoute instances. 131 | public function ConnectAsync__Handler($mode, &$data, $key, $info) 132 | { 133 | switch ($mode) 134 | { 135 | case "init": 136 | { 137 | if ($info->init) $data = $info->keep; 138 | else 139 | { 140 | $info->result = $this->Connect($info->url, $info->id, $info->timeout, $info->options, $info->web); 141 | if (!$info->result["success"]) 142 | { 143 | $info->keep = false; 144 | 145 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 146 | } 147 | else 148 | { 149 | $info->id = $info->result["id"]; 150 | $info->state = $info->result["state"]; 151 | 152 | // Move to the live queue. 153 | $data = true; 154 | } 155 | } 156 | 157 | break; 158 | } 159 | case "update": 160 | case "read": 161 | case "write": 162 | { 163 | if ($info->keep) 164 | { 165 | $info->result = $this->ProcessState($info->state); 166 | if ($info->result["success"] || $info->result["errorcode"] !== "no_data") $info->keep = false; 167 | 168 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 169 | 170 | if ($mode === "update") $data = $info->keep; 171 | } 172 | 173 | break; 174 | } 175 | case "readfps": 176 | { 177 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantRead($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 178 | 179 | break; 180 | } 181 | case "writefps": 182 | { 183 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantWrite($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 184 | 185 | break; 186 | } 187 | case "cleanup": 188 | { 189 | // When true, caller is removing. Otherwise, detaching from the queue. 190 | if ($data === true) 191 | { 192 | if (isset($info->state)) 193 | { 194 | if ($info->state->webstate["httpstate"] !== false) HTTP::ForceClose($info->state->webstate["httpstate"]); 195 | 196 | unset($info->state); 197 | } 198 | 199 | $info->keep = false; 200 | } 201 | 202 | break; 203 | } 204 | } 205 | } 206 | 207 | public function ConnectAsync($helper, $key, $callback, $url, $id = false, $timeout = false, $options = array(), $web = false) 208 | { 209 | $options["async"] = true; 210 | 211 | $info = new stdClass(); 212 | $info->init = false; 213 | $info->keep = true; 214 | $info->callback = $callback; 215 | $info->url = $url; 216 | $info->id = $id; 217 | $info->timeout = $timeout; 218 | $info->options = $options; 219 | $info->web = $web; 220 | $info->result = false; 221 | 222 | $helper->Set($key, $info, array($this, "ConnectAsync__Handler")); 223 | 224 | return array("success" => true); 225 | } 226 | 227 | public static function WRTranslate() 228 | { 229 | $args = func_get_args(); 230 | if (!count($args)) return ""; 231 | 232 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 233 | } 234 | } 235 | ?> -------------------------------------------------------------------------------- /sdk/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 | ?> -------------------------------------------------------------------------------- /sdk/support/sdk_cloud_storage_server_api_base.php: -------------------------------------------------------------------------------- 1 | web = new WebBrowser(); 19 | $this->fp = false; 20 | $this->host = false; 21 | $this->apikey = false; 22 | $this->cafile = false; 23 | $this->cacert = false; 24 | $this->cert = false; 25 | $this->apiprefix = false; 26 | } 27 | 28 | public function SetAccessInfo($host, $apikey, $cafile, $cert) 29 | { 30 | $this->web = new WebBrowser(); 31 | if (is_resource($this->fp)) @fclose($this->fp); 32 | $this->fp = false; 33 | if (substr($host, -1) === "/") $host = substr($host, 0, -1); 34 | $this->host = $host; 35 | $this->apikey = $apikey; 36 | $this->cafile = $cafile; 37 | $this->cert = $cert; 38 | } 39 | 40 | public function GetSSLInfo() 41 | { 42 | if ($this->host === false) return array("success" => false, "error" => self::CSS_Translate("Missing host."), "errorcode" => "no_access_info"); 43 | 44 | if ($this->cafile !== false && $this->cacert === false) $this->cacert = @file_get_contents($this->cafile); 45 | 46 | if (substr($this->host, 0, 7) === "http://" || RemotedAPI::IsRemoted($this->host)) 47 | { 48 | $this->cacert = ""; 49 | $this->cert = ""; 50 | } 51 | else if ($this->cacert === false || $this->cert === false) 52 | { 53 | $this->cacert = false; 54 | $this->cert = false; 55 | 56 | $this->neterror = ""; 57 | 58 | $options = array( 59 | "peer_cert_callback" => array($this, "Internal_PeerCertificateCheck"), 60 | "peer_cert_callback_opts" => "", 61 | "sslopts" => self::InitSSLOpts(array("verify_peer" => false, "capture_peer_cert_chain" => true)) 62 | ); 63 | 64 | $result = $this->web->Process($this->host . "/", $options); 65 | 66 | if (!$result["success"]) 67 | { 68 | $result["error"] .= " " . $this->neterror; 69 | 70 | return $result; 71 | } 72 | } 73 | 74 | if ($this->cert === false) return array("success" => false, "error" => self::CSS_Translate("Unable to retrieve server certificate."), "errorcode" => "cert_retrieval_failed"); 75 | if ($this->cacert === false) return array("success" => false, "error" => self::CSS_Translate("Unable to retrieve server CA certificate."), "errorcode" => "cacert_retrieval_failed"); 76 | 77 | return array("success" => true, "cacert" => $this->cacert, "cert" => $this->cert); 78 | } 79 | 80 | public function InitSSLCache($host, $cafile, $certfile) 81 | { 82 | if (!file_exists($cafile) || !file_exists($certfile)) 83 | { 84 | $this->SetAccessInfo($host, false, false, false); 85 | 86 | $result = $this->GetSSLInfo(); 87 | if (!$result["success"]) return array("success" => false, "error" => "Unable to get SSL information.", "errorcode" => "get_ssl_info_failed", "info" => $result); 88 | 89 | file_put_contents($cafile, $result["cacert"]); 90 | file_put_contents($certfile, $result["cert"]); 91 | } 92 | 93 | return array("success" => true); 94 | } 95 | 96 | protected static function CSS_Translate() 97 | { 98 | $args = func_get_args(); 99 | if (!count($args)) return ""; 100 | 101 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 102 | } 103 | 104 | // Internal function to retrieve a X509 SSL certificate during the initial connection to confirm that this is the correct target server. 105 | public function Internal_PeerCertificateCheck($type, $cert, $opts) 106 | { 107 | if (is_array($cert)) 108 | { 109 | // The server is incorrectly configured if it doesn't have the self-signed root certificate in the chain. 110 | if (count($cert) < 2) 111 | { 112 | $this->neterror = "Certificate chain is missing the root certificate. Remote host is incorrectly configured."; 113 | 114 | return false; 115 | } 116 | 117 | // The last entry is the root cert. 118 | if (!openssl_x509_export($cert[count($cert) - 1], $str)) 119 | { 120 | $this->neterror = "Certificate chain contains an invalid root certificate. Corrupted on remote host?"; 121 | 122 | return false; 123 | } 124 | 125 | $this->cacert = $str; 126 | } 127 | else 128 | { 129 | if (!openssl_x509_export($cert, $str)) 130 | { 131 | $this->neterror = "Server certificate is invalid. Corrupted on remote host?"; 132 | 133 | return false; 134 | } 135 | 136 | // Initial setup automatically trusts the SSL/TLS certificate of the host. 137 | if ($this->cert === false) $this->cert = $str; 138 | else if ($str !== $this->cert) 139 | { 140 | $this->neterror = "Certificate does not exactly match local certificate. Your client is either under a MITM attack or the remote host changed certificates."; 141 | 142 | return false; 143 | } 144 | } 145 | 146 | return true; 147 | } 148 | 149 | protected static function InitSSLOpts($options) 150 | { 151 | $result = array_merge(array( 152 | "ciphers" => "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS", 153 | "disable_compression" => true, 154 | "allow_self_signed" => false, 155 | "verify_peer_name" => false, 156 | "verify_depth" => 3, 157 | "capture_peer_cert" => true, 158 | ), $options); 159 | 160 | return $result; 161 | } 162 | 163 | protected function InitWebSocket() 164 | { 165 | if ($this->host === false || $this->apikey === false) return array("success" => false, "error" => self::CSS_Translate("Missing host or API key."), "errorcode" => "no_access_info"); 166 | if ($this->cafile === false || $this->cert === false) return array("success" => false, "error" => self::CSS_Translate("Missing SSL Certificate or Certificate Authority filename. Call GetSSLInfo() to initialize for the first time and be sure to save the results."), "errorcode" => "critical_ssl_info_missing"); 167 | 168 | $url = $this->host; 169 | 170 | // Handle Remoted API connections. 171 | if ($this->fp === false && RemotedAPI::IsRemoted($this->host)) 172 | { 173 | $result = RemotedAPI::Connect($this->host); 174 | if (!$result["success"]) return $result; 175 | 176 | $this->fp = $result["fp"]; 177 | $url = $result["url"]; 178 | 179 | $options = array( 180 | "fp" => $this->fp, 181 | "headers" => array( 182 | "X-APIKey" => $this->apikey 183 | ) 184 | ); 185 | } 186 | else 187 | { 188 | $options = array( 189 | "headers" => array( 190 | "X-APIKey" => $this->apikey 191 | ), 192 | "peer_cert_callback" => array($this, "Internal_PeerCertificateCheck"), 193 | "peer_cert_callback_opts" => "", 194 | "sslopts" => self::InitSSLOpts(array("cafile" => $this->cafile, "verify_peer" => true)) 195 | ); 196 | } 197 | 198 | if (!class_exists("WebSocket", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/websocket.php"; 199 | 200 | $ws = new WebSocket(); 201 | 202 | $wsurl = str_replace(array("https://", "http://"), array("wss://", "ws://"), $url); 203 | $result = $ws->Connect($wsurl . $this->apiprefix, $url, $options); 204 | if (!$result["success"]) return $result; 205 | 206 | $result["ws"] = $ws; 207 | 208 | return $result; 209 | } 210 | 211 | protected function RunAPI($method, $apipath, $options = array(), $expected = 200, $encodejson = true, $decodebody = true) 212 | { 213 | if ($this->host === false || $this->apikey === false) return array("success" => false, "error" => self::CSS_Translate("Missing host or API key."), "errorcode" => "no_access_info"); 214 | if ($this->cafile === false || $this->cert === false) return array("success" => false, "error" => self::CSS_Translate("Missing SSL Certificate or Certificate Authority filename. Call GetSSLInfo() to initialize for the first time and be sure to save the results."), "errorcode" => "critical_ssl_info_missing"); 215 | 216 | $url = $this->host; 217 | 218 | // Handle Remoted API connections. 219 | if ($this->fp === false && RemotedAPI::IsRemoted($this->host)) 220 | { 221 | $result = RemotedAPI::Connect($this->host); 222 | if (!$result["success"]) return $result; 223 | 224 | $this->fp = $result["fp"]; 225 | $url = $result["url"]; 226 | 227 | $options2 = array( 228 | "fp" => $this->fp, 229 | "method" => $method, 230 | "headers" => array( 231 | "Connection" => "keep-alive", 232 | "X-APIKey" => $this->apikey 233 | ) 234 | ); 235 | } 236 | else if ($this->fp !== false) 237 | { 238 | $url = RemotedAPI::ExtractRealHost($url); 239 | 240 | $options2 = array( 241 | "fp" => $this->fp, 242 | "method" => $method, 243 | "headers" => array( 244 | "Connection" => "keep-alive" 245 | ) 246 | ); 247 | } 248 | else 249 | { 250 | $options2 = array( 251 | "method" => $method, 252 | "headers" => array( 253 | "Connection" => "keep-alive", 254 | "X-APIKey" => $this->apikey 255 | ), 256 | "peer_cert_callback" => array($this, "Internal_PeerCertificateCheck"), 257 | "peer_cert_callback_opts" => "", 258 | "sslopts" => self::InitSSLOpts(array("cafile" => $this->cafile, "verify_peer" => true)) 259 | ); 260 | } 261 | 262 | if ($encodejson && $method !== "GET") 263 | { 264 | $options2["headers"]["Content-Type"] = "application/json"; 265 | $options2["body"] = json_encode($options); 266 | } 267 | else 268 | { 269 | $options2 = array_merge($options2, $options); 270 | } 271 | 272 | $result = $this->web->Process($url . $this->apiprefix . "/" . $apipath, $options2); 273 | 274 | if (!$result["success"] && $this->fp !== false) 275 | { 276 | // If the server terminated the connection, then re-establish the connection and rerun the request. 277 | if (is_resource($this->fp)) @fclose($this->fp); 278 | $this->fp = false; 279 | 280 | return $this->RunAPI($method, $apipath, $options, $expected, $encodejson, $decodebody); 281 | } 282 | 283 | if (!$result["success"]) return $result; 284 | 285 | if (isset($result["fp"]) && is_resource($result["fp"])) $this->fp = $result["fp"]; 286 | else $this->fp = false; 287 | 288 | // Cloud Storage Server always responds with 400 Bad Request for errors. Attempt to decode the error. 289 | if ($result["response"]["code"] == 400) 290 | { 291 | $error = @json_decode($result["body"], true); 292 | if (is_array($error) && isset($error["success"]) && !$error["success"]) return $error; 293 | } 294 | 295 | if ($result["response"]["code"] != $expected) return array("success" => false, "error" => self::CSS_Translate("Expected a %d response from Cloud Storage Server. Received '%s'.", $expected, $result["response"]["line"]), "errorcode" => "unexpected_cloud_storage_server_response", "info" => $result); 296 | 297 | if ($decodebody) $result["body"] = json_decode($result["body"], true); 298 | 299 | return $result; 300 | } 301 | } 302 | ?> -------------------------------------------------------------------------------- /sdk/support/random.php: -------------------------------------------------------------------------------- 1 | mode = false; 15 | $this->fp = false; 16 | $this->cryptosafe = $cryptosafe; 17 | 18 | // Native first (PHP 7 and later). 19 | if (function_exists("random_bytes")) $this->mode = "native"; 20 | 21 | // OpenSSL fallback. 22 | if ($this->mode === false && function_exists("openssl_random_pseudo_bytes")) 23 | { 24 | // PHP 5.4.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for performance. 25 | @openssl_random_pseudo_bytes(4, $strong); 26 | if ($strong) $this->mode = "openssl"; 27 | } 28 | 29 | // Locate a (relatively) suitable source of entropy or raise an exception. 30 | if (strtoupper(substr(PHP_OS, 0, 3)) === "WIN") 31 | { 32 | // PHP 5.3.0 introduced native Windows CryptGenRandom() integration via php_win32_get_random_bytes() for functionality. 33 | if ($this->mode === false && PHP_VERSION_ID > 50300 && function_exists("mcrypt_create_iv")) $this->mode = "mcrypt"; 34 | } 35 | else 36 | { 37 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/arandom")) 38 | { 39 | // OpenBSD. mcrypt doesn't attempt to use this despite claims of higher quality entropy with performance. 40 | $this->fp = @fopen("/dev/arandom", "rb"); 41 | if ($this->fp !== false) $this->mode = "file"; 42 | } 43 | 44 | if ($cryptosafe && $this->mode === false && file_exists("/dev/random")) 45 | { 46 | // Everything else. 47 | $this->fp = @fopen("/dev/random", "rb"); 48 | if ($this->fp !== false) $this->mode = "file"; 49 | } 50 | 51 | if (!$cryptosafe && $this->mode === false && file_exists("/dev/urandom")) 52 | { 53 | // Everything else. 54 | $this->fp = @fopen("/dev/urandom", "rb"); 55 | if ($this->fp !== false) $this->mode = "file"; 56 | } 57 | 58 | if ($this->mode === false && function_exists("mcrypt_create_iv")) 59 | { 60 | // mcrypt_create_iv() is last because it opens and closes a file handle every single call. 61 | $this->mode = "mcrypt"; 62 | } 63 | } 64 | 65 | // Throw an exception if unable to find a suitable entropy source. 66 | if ($this->mode === false) 67 | { 68 | throw new Exception(self::RNG_Translate("Unable to locate a suitable entropy source.")); 69 | exit(); 70 | } 71 | } 72 | 73 | public function __destruct() 74 | { 75 | if ($this->mode === "file") fclose($this->fp); 76 | } 77 | 78 | public function GetBytes($length) 79 | { 80 | if ($this->mode === false) return false; 81 | 82 | $length = (int)$length; 83 | if ($length < 1) return false; 84 | 85 | $result = ""; 86 | do 87 | { 88 | switch ($this->mode) 89 | { 90 | case "native": $data = @random_bytes($length); break; 91 | case "openssl": $data = @openssl_random_pseudo_bytes($length, $strong); if (!$strong) $data = false; break; 92 | case "mcrypt": $data = @mcrypt_create_iv($length, ($this->cryptosafe ? MCRYPT_DEV_RANDOM : MCRYPT_DEV_URANDOM)); break; 93 | case "file": $data = @fread($this->fp, $length); break; 94 | default: $data = false; 95 | } 96 | if ($data === false) return false; 97 | 98 | $result .= $data; 99 | } while (strlen($result) < $length); 100 | 101 | return substr($result, 0, $length); 102 | } 103 | 104 | public function GenerateToken($length = 64) 105 | { 106 | $data = $this->GetBytes($length); 107 | if ($data === false) return false; 108 | 109 | return bin2hex($data); 110 | } 111 | 112 | // Get a random number between $min and $max (inclusive). 113 | public function GetInt($min, $max) 114 | { 115 | $min = (int)$min; 116 | $max = (int)$max; 117 | if ($max < $min) return false; 118 | if ($min == $max) return $min; 119 | 120 | $range = $max - $min + 1; 121 | 122 | $bits = 1; 123 | while ((1 << $bits) <= $range) $bits++; 124 | 125 | $numbytes = (int)(($bits + 7) / 8); 126 | $mask = (1 << $bits) - 1; 127 | 128 | do 129 | { 130 | $data = $this->GetBytes($numbytes); 131 | if ($data === false) return false; 132 | 133 | $result = 0; 134 | for ($x = 0; $x < $numbytes; $x++) 135 | { 136 | $result = ($result * 256) + ord($data[$x]); 137 | } 138 | 139 | $result = $result & $mask; 140 | } while ($result >= $range); 141 | 142 | return $result + $min; 143 | } 144 | 145 | // Convenience method to generate a random alphanumeric string. 146 | public function GenerateString($size = 32) 147 | { 148 | $result = ""; 149 | for ($x = 0; $x < $size; $x++) 150 | { 151 | $data = $this->GetInt(0, 61); 152 | if ($data === false) return false; 153 | 154 | $result .= self::$alphanum[$data]; 155 | } 156 | 157 | return $result; 158 | } 159 | 160 | public function GenerateWordLite(&$freqmap, $len) 161 | { 162 | $totalc = 0; 163 | $totalv = 0; 164 | foreach ($freqmap["consonants"] as $chr => $num) $totalc += $num; 165 | foreach ($freqmap["vowels"] as $chr => $num) $totalv += $num; 166 | 167 | if ($totalc <= 0 || $totalv <= 0) return false; 168 | 169 | $result = ""; 170 | for ($x = 0; $x < $len; $x++) 171 | { 172 | if ($x % 2) 173 | { 174 | $data = $this->GetInt(0, $totalv - 1); 175 | if ($data === false) return false; 176 | 177 | foreach ($freqmap["vowels"] as $chr => $num) 178 | { 179 | if ($num > $data) 180 | { 181 | $result .= $chr; 182 | 183 | break; 184 | } 185 | 186 | $data -= $num; 187 | } 188 | } 189 | else 190 | { 191 | $data = $this->GetInt(0, $totalc - 1); 192 | if ($data === false) return false; 193 | 194 | foreach ($freqmap["consonants"] as $chr => $num) 195 | { 196 | if ($num > $data) 197 | { 198 | $result .= $chr; 199 | 200 | break; 201 | } 202 | 203 | $data -= $num; 204 | } 205 | } 206 | } 207 | 208 | return $result; 209 | } 210 | 211 | public function GenerateWord(&$freqmap, $len, $separator = "-") 212 | { 213 | $result = ""; 214 | $queue = array(); 215 | $threshold = $freqmap["threshold"]; 216 | $state = "start"; 217 | while ($len) 218 | { 219 | //echo $state . " - " . $len . ": " . $result . "\n"; 220 | switch ($state) 221 | { 222 | case "start": 223 | { 224 | // The start of the word (or restart). 225 | $path = &$freqmap["start"]; 226 | while (count($queue) < $threshold && $len) 227 | { 228 | if ($len > 1 || !$path["*"]) 229 | { 230 | // Some part of the word. 231 | $found = false; 232 | if ($path[""]) 233 | { 234 | $pos = $this->GetInt(0, $path[""] - 1); 235 | 236 | foreach ($path as $chr => &$info) 237 | { 238 | if (!is_array($info)) continue; 239 | 240 | if ($info["+"] > $pos) 241 | { 242 | $result .= $chr; 243 | $queue[] = $chr; 244 | $path = &$path[$chr]; 245 | $len--; 246 | 247 | $found = true; 248 | 249 | break; 250 | } 251 | 252 | $pos -= $info["+"]; 253 | } 254 | } 255 | 256 | if (!$found) 257 | { 258 | $state = (count($queue) ? "recovery" : "restart"); 259 | 260 | break; 261 | } 262 | } 263 | else 264 | { 265 | // Last letter of the word. 266 | $found = false; 267 | if ($path["*"]) 268 | { 269 | $pos = $this->GetInt(0, $path["*"] - 1); 270 | 271 | foreach ($path as $chr => &$info) 272 | { 273 | if (!is_array($info)) continue; 274 | 275 | if ($info["-"] > $pos) 276 | { 277 | $result .= $chr; 278 | $queue[] = $chr; 279 | $path = &$path[$chr]; 280 | $len--; 281 | 282 | $found = true; 283 | 284 | break; 285 | } 286 | 287 | $pos -= $info["-"]; 288 | } 289 | } 290 | 291 | if (!$found) 292 | { 293 | $state = (count($queue) ? "end" : "restart"); 294 | 295 | break; 296 | } 297 | } 298 | } 299 | 300 | if (count($queue) >= $threshold) $state = ($len >= $threshold ? "middle" : "end"); 301 | 302 | break; 303 | } 304 | case "middle": 305 | { 306 | // The middle of the word. 307 | $str = implode("", $queue); 308 | 309 | if (!isset($freqmap["middle"][$str])) $state = "recovery"; 310 | else 311 | { 312 | $found = false; 313 | 314 | if ($freqmap["middle"][$str][""]) 315 | { 316 | $pos = $this->GetInt(0, $freqmap["middle"][$str][""] - 1); 317 | 318 | foreach ($freqmap["middle"][$str] as $chr => $num) 319 | { 320 | if ($chr === "") continue; 321 | 322 | if ($num > $pos) 323 | { 324 | $result .= $chr; 325 | $queue[] = $chr; 326 | array_shift($queue); 327 | $len--; 328 | 329 | if ($len < $threshold) $state = "end"; 330 | 331 | $found = true; 332 | 333 | break; 334 | } 335 | 336 | $pos -= $num; 337 | } 338 | } 339 | 340 | if (!$found) $state = "recovery"; 341 | } 342 | 343 | break; 344 | } 345 | case "end": 346 | { 347 | if (!isset($freqmap["end"][$len]) || !count($queue) || !isset($freqmap["end"][$len][$queue[count($queue) - 1]])) $state = "restart"; 348 | else 349 | { 350 | $path = &$freqmap["end"][$len][$queue[count($queue) - 1]]; 351 | 352 | $found = false; 353 | 354 | if ($path[""]) 355 | { 356 | $pos = $this->GetInt(0, $path[""] - 1); 357 | 358 | foreach ($path as $str => $num) 359 | { 360 | if ($str === "") continue; 361 | 362 | if ($num > $pos) 363 | { 364 | $result .= $str; 365 | $len = 0; 366 | 367 | $found = true; 368 | 369 | break; 370 | } 371 | 372 | $pos -= $num; 373 | } 374 | } 375 | 376 | if (!$found) $state = "restart"; 377 | } 378 | 379 | break; 380 | } 381 | case "recovery": 382 | { 383 | if (!count($queue) || !isset($freqmap["recovery"][$queue[count($queue) - 1]])) $state = "restart"; 384 | else 385 | { 386 | $path = &$freqmap["recovery"][$queue[count($queue) - 1]]; 387 | 388 | $found = false; 389 | 390 | if ($path[""]) 391 | { 392 | $pos = $this->GetInt(0, $path[""] - 1); 393 | 394 | foreach ($path as $chr => $num) 395 | { 396 | if ($chr === "") continue; 397 | 398 | if ($num > $pos) 399 | { 400 | $result .= $chr; 401 | $queue[] = $chr; 402 | array_shift($queue); 403 | $len--; 404 | 405 | $state = ($len >= $threshold ? "middle" : "end"); 406 | 407 | $found = true; 408 | 409 | break; 410 | } 411 | 412 | $pos -= $num; 413 | } 414 | } 415 | 416 | if (!$found) $state = "restart"; 417 | } 418 | 419 | break; 420 | } 421 | case "restart": 422 | { 423 | $result .= $separator; 424 | $queue = array(); 425 | $len -= strlen($separator); 426 | 427 | $state = "start"; 428 | 429 | break; 430 | } 431 | } 432 | } 433 | 434 | return $result; 435 | } 436 | 437 | public function GetMode() 438 | { 439 | return $this->mode; 440 | } 441 | 442 | protected static function RNG_Translate() 443 | { 444 | $args = func_get_args(); 445 | if (!count($args)) return ""; 446 | 447 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 448 | } 449 | } 450 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cloud Storage Server /scripts Extension 2 | ======================================= 3 | 4 | A powerful and flexible cross-platform /scripts extension for the [self-hosted cloud storage API](https://github.com/cubiclesoft/cloud-storage-server) for starting and monitoring long-running scripts. Includes a PHP SDK for interacting with the /scripts API. 5 | 6 | The /scripts extension is useful for starting long-running scripts that take a while to complete and tracking completion status, running scripts as other users (e.g. root/SYSTEM), and notifying other systems that are monitoring for script completions that they can start doing work immediately instead of polling every 1-5 minutes and being told by the remote system that there is nothing to do. 7 | 8 | NOTE: This extension is considered largely obsolete in favor of [xcron](https://github.com/cubiclesoft/xcron), which can do most of the same things but far more elegantly and with a richer feature set. 9 | 10 | [![Donate](https://cubiclesoft.com/res/donate-shield.png)](https://cubiclesoft.com/donate/) [![Discord](https://img.shields.io/discord/777282089980526602?label=chat&logo=discord)](https://cubiclesoft.com/product-support/github/) 11 | 12 | Features 13 | -------- 14 | 15 | * Cross-platform support for all major platforms, including Windows. 16 | * Script queues and limits on how many of each script can be running at one time. 17 | * Tracks completion status of tasks and subtasks within each script. 18 | * Logs each script run in a SQLite database. Useful for later analysis. 19 | * Uses a crontab-like format, per-user definition file to define what scripts can run. 20 | * Supports passing parameters and limited 'stdin' to the target script. 21 | * Run scripts as other users and groups on the system (*NIX only). 22 | * RESTful status checking and live WebSocket monitoring support. 23 | * Also has a liberal open source license. MIT or LGPL, your choice. 24 | * Designed for relatively painless integration into your environment. 25 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 26 | 27 | A Note On Security 28 | ------------------ 29 | 30 | This extension is meant to be running on a [Cloud Storage Server](https://github.com/cubiclesoft/cloud-storage-server) that is running as the root/SYSTEM user. As such, it should be installed on an isolated Cloud Storage Server instance and on a different port if another instance is running on the same machine. Then be sure to firewall the server running this extension so that only those systems that really need access can access it. 31 | 32 | Leaving a root/SYSTEM user Cloud Storage Server open to the whole Internet or a large network is a bad idea. With great power comes great responsibility. 33 | 34 | Installation 35 | ------------ 36 | 37 | Extract and copy the Cloud Storage Server files as you would for a server. Remove the `/server_exts/files.php` file. Copy the `/server_exts/scripts.php` file to the `/server_exts` directory. Now install the Cloud Storage Server under the root/SYSTEM user and set up your firewall to isolate the system as per the security note above. 38 | 39 | If you are running Cloud Storage Server on a Windows or Windows Server OS, you will also need to copy the contents of the `/support` directory from this extension to the Cloud Storage Server `/support` directory. It contains [createprocess.exe](https://github.com/cubiclesoft/createprocess-windows), which is used as an intermediate layer between PHP and the running script due to [PHP Bug #47918](https://bugs.php.net/bug.php?id=47918). 40 | 41 | You may find the cross-platform [Service Manager](https://github.com/cubiclesoft/service-manager/) tool to be useful to enable Cloud Storage Server to function as a system service. 42 | 43 | Be sure to create a user using Cloud Storage Server `manage.php` and add the /scripts extension to the user account. 44 | 45 | Next, you'll need to initialize the user account's /scripts extension. To do this, use the PHP SDK to run a script like: 46 | 47 | ````php 48 | SetAccessInfo("http://127.0.0.1:9893", "YOUR_API_KEY", "", ""); 53 | 54 | $result = $css->RunScript("test"); 55 | if (!$result["success"]) 56 | { 57 | var_dump($result); 58 | exit(); 59 | } 60 | 61 | $id = $result["body"]["id"]; 62 | 63 | do 64 | { 65 | sleep(3); 66 | $result = $css->GetStatus($id); 67 | if (!$result["success"]) 68 | { 69 | var_dump($result); 70 | exit(); 71 | } 72 | 73 | var_dump(file_exists("D:/somepathhere/1/scripts/status/" . $id . ".json")); 74 | var_dump($result["body"]); 75 | } while ($result["body"]["state"] !== "done"); 76 | ?> 77 | ```` 78 | 79 | The above will attempt to start a script with the name of "test" but, since the "exectab.txt" file for the user will be empty at first, it will bail out fairly early on with an 'invalid_name' error. At this point, the installation is complete and it is time to move onto setting up your first script in 'exectab.txt'. 80 | 81 | The exectab.txt File Format 82 | --------------------------- 83 | 84 | Locate the Cloud Storage Server storage directory as specified by the configuration. Within it are user ID directories. Within a user ID directory is a set of other directories associated with each enabled and used extension. Find the newly set up user account and the /scripts directory. Within the /scripts directory is a SQLite database that tracks all script runs, a /status subdirectory, and a file called 'exectab.txt'. The 'exectab.txt' file is very similar to crontab except it doesn't have anything to do with scheduling. 85 | 86 | Example 'exectab.txt' file: 87 | 88 | ```` 89 | # Run a PHP script as a specific user and group with a limit of five simultaneous runs of this script running at the same time (unlimited queue size). 90 | -u=someuser -g=somegroup -s=5 test /usr/bin/php /var/scripts/myscript.php -o=@@1 @@2 91 | 92 | # Run a PHP script as the same user/group as the Cloud Storage Server process (probably root) with a limit of one script running at a time and with a starting directory of /var/log/apache2. 93 | -d=/var/log/apache2 test2 /usr/bin/php /var/scripts/myscript2.php 94 | 95 | # Does not start any processes but will notify listeners (via /scripts/v1/monitor) that 'test3' has finished running. 96 | test3 97 | 98 | # Does not start any processes but passes unescaped parameters to 'test4'. 99 | -n test4 @@1 @@2 100 | ```` 101 | 102 | The above defines several script names: `test`, `test2`, `test3`, and `test4`. Each one does something different. The format for script execution lines is: 103 | 104 | `[options] scriptname [executable [params]]` 105 | 106 | The full list of options for scripts is: 107 | 108 | * -d=startdir - The starting directory for the target process. 109 | * -e=envvar - An environment variable to set for the target process. 110 | * -g=group - The *NIX group to run the process under (*NIX only). 111 | * -i - Allow 'stdin' passthrough. Without this option, passing non-empty 'stdin' strings to /scripts/v1/run is an error. 112 | * -m=num - Maximum queue length. Default is unlimited. 113 | * -n - No process execution. No parameter escaping. Useful for passing parameters to monitors. 114 | * -r - Remove successful and cancelled script runs from the log. The status/result won't be available after completion. 115 | * -s=num - The number of items in the queue that may run simultaneously. Default is 1. 116 | * -u=user - The *NIX user to run the process under (*NIX only). 117 | 118 | Parameters passed to the /scripts/v1/run API may be referenced using the @@ prefix starting at @@1. All modified parameters are sanitized using escapeshellarg() before they are executed to avoid security issues with untrusted user input. 119 | 120 | The 'stdin' passthrough is limited to approximately 1MB of data. For most purposes, this limit is more than sufficient. 121 | 122 | Tracking Progress 123 | ----------------- 124 | 125 | While a process is running, it may write to either stdout or stderr. The /scripts extension looks at each line of output for lines that start with brackets. Depending on usage, the line will indicate a task or a subtask. 126 | 127 | ```` 128 | [1/3] Task 1 129 | [50%] Subtask 130 | [88%] Subtask 131 | [2] Task 2 132 | [15%] Subtask 133 | [45%] Subtask 134 | [98%] Subtask 135 | [3] Task 3 136 | ```` 137 | 138 | The first line specifies that it is the first of three tasks. The rest of the line after the part in brackets becomes the task title/name. The second line specifies a '%' sign before the closing bracket, which means that line is a subtask of the main task. The title/name for a subtask is usually something like "Processing...". 139 | 140 | The title/name portion of a task/subtask is made available to via /scripts/v1/status API, which could be used, for example, to track progress in a web application. Be sure to avoid exposing internal details of the server to the /scripts API. All other lines of output are ignored by the /scripts extension, which allows most debugging information to be left in just in case the process needs to be run manually to diagnose some issue. 141 | 142 | Processes can be queued to launch in the future at a specified time. Queued processes that have not started running can be cancelled with the /scripts/v1/cancel API. 143 | 144 | Localhost Performance 145 | --------------------- 146 | 147 | If a script is started via /scripts on the same host as a web server (i.e. the web server uses the API to start the script), it is possible to avoid using the /scripts/v1/status API to query the status. As the script runs, the information is dumped into the /scripts/status subdirectory for the user account as a JSON file. 148 | 149 | To improve performance and avoid using the SDK, first attempt to load the file directly and parse it as JSON. If that fails for any reason, fallback to the SDK to get the status. There are a few reasons the attempt to load the file might fail such as attempting to load and read the file while it is being written to disk resulting in an incomplete JSON object or the process might have ended and the file no longer exists. 150 | 151 | Monitoring 152 | ---------- 153 | 154 | Let's say you have a server behind a firewall on a corporate network (server A) and you have another server in your DMZ (server B). A user connects to server B and performs a task. A cron job runs on server A every couple of minutes and polls server B to see if it has anything to do. About 99% of the time, server B responds that there is nothing to do. Therefore, the script on server A does nothing and simply terminates. This repeats ad nauseum until the end of time, wasting bandwidth and, if web servers had feelings, server B would find server A to be rather annoying. "Do you have anything for me? No. Do you have anything for me? No. Do you have anything for me? No! ..." In addition, users of the system are secretly miffed because they have to wait for the cron job to run before stuff happens. 155 | 156 | The /scripts/v1/monitor API lets server A establish a WebSocket connection to server B and watch a specific script name. Whenever that script name runs and completes via the /scripts extension, server B notifies server A immediately. This API has several advantages: It stops wasting perfectly good bandwidth, establishes fewer TCP/IP connections, and users are happier because the overall system appears to be more responsive. 157 | 158 | Example monitoring script: 159 | 160 | ````php 161 | InitSSLCache("https://remoteserver.com:9892", $rootpath . "/cache/css_ca.pem", $rootpath . "/cache/css_cert.pem"); 171 | if (!$result["success"]) 172 | { 173 | var_dump($result); 174 | exit(); 175 | } 176 | 177 | $css->SetAccessInfo("https://remoteserver.com:9892", "YOUR_API_KEY", $rootpath . "/cache/css_ca.pem", file_get_contents($rootpath . "/cache/css_cert.pem")); 178 | 179 | $result = $css->StartMonitoring("test"); 180 | if (!$result["success"]) 181 | { 182 | var_dump($result); 183 | exit(); 184 | } 185 | 186 | $ws = $result["ws"]; 187 | $api_sequence = $result["api_sequence"]; 188 | 189 | // Main loop. 190 | $result = $ws->Wait(); 191 | while ($result["success"]) 192 | { 193 | do 194 | { 195 | $result = $ws->Read(); 196 | if (!$result["success"]) break; 197 | 198 | if ($result["data"] !== false) 199 | { 200 | $data = json_decode($result["data"]["payload"], true); 201 | 202 | var_dump($data); 203 | if ($data["state"] === "done") 204 | { 205 | // Do something here... 206 | } 207 | } 208 | } while ($result["data"] !== false); 209 | 210 | $result = $ws->Wait(); 211 | } 212 | 213 | // An error occurred. 214 | var_dump($result); 215 | ?> 216 | ```` 217 | 218 | There are various ways to get the monitoring script to start. If you want to start it at system boot and keep it going should the script terminate at some point, the cross-platform [Service Manager](https://github.com/cubiclesoft/service-manager/) tool is useful. 219 | 220 | Extension: /scripts 221 | -------------------- 222 | 223 | The /scripts extension implements the /scripts/v1 API. To try to keep this page relatively short, here is the list of available APIs, the input request method, and successful return values (always JSON output): 224 | 225 | POST /scripts/v1/run 226 | 227 | * name - Script name 228 | * args - An array of arguments to use to replace @@ tokens with 229 | * stdin - Data to pass through to stdin of the process 230 | * queue - Optional UNIX timestamp (integer) to specify when to start the process 231 | * Returns: success (boolean), id (string), name (string), state (string), queued (integer, UNIX timestamp), position (integer) 232 | 233 | POST /scripts/v1/cancel/ID 234 | 235 | * ID - Script run ID 236 | * Returns: success (boolean) 237 | * Summary: Cancels a previously queued script that has not started running 238 | 239 | GET /scripts/v1/status[/ID] 240 | 241 | * ID - Script run ID 242 | * Returns (with ID): success (boolean), id (string), name (string), state (string), additional info (various) 243 | * Returns (without ID): success (boolean), queued (object), running (object) 244 | 245 | GET /scripts/v1/monitor (WebSocket only) 246 | 247 | * name - Script name OR empty string to monitor all names 248 | * Returns: success (boolean), name (string), enabled (boolean) 249 | 250 | GET /scripts/v1/guest/list 251 | 252 | * Returns: success (boolean), guests (array) 253 | 254 | POST /scripts/v1/guest/create 255 | 256 | * name - Script name 257 | * run - Guest can run scripts 258 | * cancel - Guest can cancel queued scripts 259 | * status - Guest can retrieve the status of scripts 260 | * monitor - Guest can live monitor scripts 261 | * expires - Unix timestamp (integer) 262 | * Returns: success (boolean), id (string), info (array) 263 | * Summary: The 'info' array contains: apikey, created, expires, info (various) 264 | 265 | POST /scripts/v1/guest/delete/ID 266 | 267 | * ID - Guest ID 268 | * Returns: success (boolean) 269 | -------------------------------------------------------------------------------- /sdk/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 | ?> -------------------------------------------------------------------------------- /sdk/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 | ?> -------------------------------------------------------------------------------- /sdk/support/web_browser.php: -------------------------------------------------------------------------------- 1 | ResetState(); 15 | $this->SetState($prevstate); 16 | $this->html = false; 17 | } 18 | 19 | public function ResetState() 20 | { 21 | $this->data = array( 22 | "allowedprotocols" => array("http" => true, "https" => true), 23 | "allowedredirprotocols" => array("http" => true, "https" => true), 24 | "hostauths" => array(), 25 | "cookies" => array(), 26 | "referer" => "", 27 | "autoreferer" => true, 28 | "useragent" => "firefox", 29 | "followlocation" => true, 30 | "maxfollow" => 20, 31 | "extractforms" => false, 32 | "httpopts" => array(), 33 | ); 34 | } 35 | 36 | public function SetState($options = array()) 37 | { 38 | $this->data = array_merge($this->data, $options); 39 | } 40 | 41 | public function GetState() 42 | { 43 | return $this->data; 44 | } 45 | 46 | public function ProcessState(&$state) 47 | { 48 | while ($state["state"] !== "done") 49 | { 50 | switch ($state["state"]) 51 | { 52 | case "initialize": 53 | { 54 | if (!isset($this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) || !$this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) 55 | { 56 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed in '%s'.", $state["urlinfo"]["scheme"], $state["url"]), "errorcode" => "allowed_protocols"); 57 | } 58 | 59 | $filename = HTTP::ExtractFilename($state["urlinfo"]["path"]); 60 | $pos = strrpos($filename, "."); 61 | $fileext = ($pos !== false ? strtolower(substr($filename, $pos + 1)) : ""); 62 | 63 | // Set up some standard headers. 64 | $headers = array(); 65 | $profile = strtolower($state["profile"]); 66 | $tempprofile = explode("-", $profile); 67 | if (count($tempprofile) == 2) 68 | { 69 | $profile = $tempprofile[0]; 70 | $fileext = $tempprofile[1]; 71 | } 72 | if (substr($profile, 0, 2) == "ie" || ($profile == "auto" && substr($this->data["useragent"], 0, 2) == "ie")) 73 | { 74 | if ($fileext == "css") $headers["Accept"] = "text/css"; 75 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5"; 76 | else if ($fileext == "js") $headers["Accept"] = "application/javascript, */*;q=0.8"; 77 | else if ($this->data["referer"] != "" || $fileext == "" || $fileext == "html" || $fileext == "xhtml" || $fileext == "xml") $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 78 | else $headers["Accept"] = "*/*"; 79 | 80 | $headers["Accept-Language"] = "en-US"; 81 | $headers["User-Agent"] = HTTP::GetUserAgent(substr($profile, 0, 2) == "ie" ? $profile : $this->data["useragent"]); 82 | } 83 | else if ($profile == "firefox" || ($profile == "auto" && $this->data["useragent"] == "firefox")) 84 | { 85 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 86 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png,image/*;q=0.8,*/*;q=0.5"; 87 | else if ($fileext == "js") $headers["Accept"] = "*/*"; 88 | else $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 89 | 90 | $headers["Accept-Language"] = "en-us,en;q=0.5"; 91 | $headers["Cache-Control"] = "max-age=0"; 92 | $headers["User-Agent"] = HTTP::GetUserAgent("firefox"); 93 | } 94 | else if ($profile == "opera" || ($profile == "auto" && $this->data["useragent"] == "opera")) 95 | { 96 | // Opera has the right idea: Just send the same thing regardless of the request type. 97 | $headers["Accept"] = "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"; 98 | $headers["Accept-Language"] = "en-US,en;q=0.9"; 99 | $headers["Cache-Control"] = "no-cache"; 100 | $headers["User-Agent"] = HTTP::GetUserAgent("opera"); 101 | } 102 | else if ($profile == "safari" || $profile == "edge" || $profile == "chrome" || ($profile == "auto" && ($this->data["useragent"] == "safari" || $this->data["useragent"] == "edge" || $this->data["useragent"] == "chrome"))) 103 | { 104 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 105 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg" || $fileext == "js") $headers["Accept"] = "*/*"; 106 | else $headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; 107 | 108 | $headers["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"; 109 | $headers["Accept-Language"] = "en-US,en;q=0.8"; 110 | $headers["User-Agent"] = HTTP::GetUserAgent($profile == "safari" || $profile == "chrome" ? $profile : $this->data["useragent"]); 111 | } 112 | 113 | if ($this->data["referer"] != "") $headers["Referer"] = $this->data["referer"]; 114 | 115 | // Generate the final headers array. 116 | $headers = array_merge($headers, $state["httpopts"]["headers"], $state["tempoptions"]["headers"]); 117 | 118 | // Calculate the host and reverse host and remove port information. 119 | $host = (isset($headers["Host"]) ? $headers["Host"] : $state["urlinfo"]["host"]); 120 | $pos = strpos($host, "]"); 121 | if (substr($host, 0, 1) == "[" && $pos !== false) 122 | { 123 | $host = substr($host, 0, $pos + 1); 124 | } 125 | else 126 | { 127 | $pos = strpos($host, ":"); 128 | if ($pos !== false) $host = substr($host, 0, $pos); 129 | } 130 | $dothost = $host; 131 | $dothost = strtolower($dothost); 132 | if (substr($dothost, 0, 1) != ".") $dothost = "." . $dothost; 133 | $state["dothost"] = $dothost; 134 | 135 | // Append Authorization header. 136 | if (isset($headers["Authorization"])) $this->data["hostauths"][$host] = $headers["Authorization"]; 137 | else if (isset($this->data["hostauths"][$host])) $headers["Authorization"] = $this->data["hostauths"][$host]; 138 | 139 | // Append cookies and delete old, invalid cookies. 140 | $secure = ($state["urlinfo"]["scheme"] == "https"); 141 | $cookiepath = $state["urlinfo"]["path"]; 142 | if ($cookiepath == "") $cookiepath = "/"; 143 | $pos = strrpos($cookiepath, "/"); 144 | if ($pos !== false) $cookiepath = substr($cookiepath, 0, $pos + 1); 145 | $state["cookiepath"] = $cookiepath; 146 | $cookies = array(); 147 | foreach ($this->data["cookies"] as $domain => $paths) 148 | { 149 | if (strlen($dothost) >= strlen($domain) && substr($dothost, -strlen($domain)) === $domain) 150 | { 151 | foreach ($paths as $path => $cookies2) 152 | { 153 | if (substr($cookiepath, 0, strlen($path)) == $path) 154 | { 155 | foreach ($cookies2 as $num => $info) 156 | { 157 | if (isset($info["expires_ts"]) && $this->GetExpiresTimestamp($info["expires_ts"]) < time()) unset($this->data["cookies"][$domain][$path][$num]); 158 | else if ($secure || !isset($info["secure"])) $cookies[$info["name"]] = $info["value"]; 159 | } 160 | 161 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 162 | } 163 | } 164 | 165 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 166 | } 167 | } 168 | 169 | $cookies2 = array(); 170 | foreach ($cookies as $name => $value) $cookies2[] = rawurlencode($name) . "=" . rawurlencode($value); 171 | $headers["Cookie"] = implode("; ", $cookies2); 172 | if ($headers["Cookie"] == "") unset($headers["Cookie"]); 173 | 174 | // Generate the final options array. 175 | $state["options"] = array_merge($state["httpopts"], $state["tempoptions"]); 176 | $state["options"]["headers"] = $headers; 177 | if ($state["timeout"] !== false) $state["options"]["timeout"] = HTTP::GetTimeLeft($state["startts"], $state["timeout"]); 178 | 179 | // Let a callback handle any additional state changes. 180 | if (isset($state["options"]["pre_retrievewebpage_callback"]) && is_callable($state["options"]["pre_retrievewebpage_callback"]) && !call_user_func_array($state["options"]["pre_retrievewebpage_callback"], array(&$state))) 181 | { 182 | return array("success" => false, "error" => self::WBTranslate("Pre-RetrieveWebpage callback returned with a failure condition for '%s'.", $state["url"]), "errorcode" => "pre_retrievewebpage_callback"); 183 | } 184 | 185 | // Process the request. 186 | $result = HTTP::RetrieveWebpage($state["url"], $state["options"]); 187 | $result["url"] = $state["url"]; 188 | unset($state["options"]["files"]); 189 | unset($state["options"]["body"]); 190 | unset($state["tempoptions"]["headers"]["Content-Type"]); 191 | $result["options"] = $state["options"]; 192 | $result["firstreqts"] = $state["startts"]; 193 | $result["numredirects"] = $state["numredirects"]; 194 | $result["redirectts"] = $state["redirectts"]; 195 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 196 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 197 | if (!$result["success"]) return array("success" => false, "error" => self::WBTranslate("Unable to retrieve content. %s", $result["error"]), "info" => $result, "state" => $state, "errorcode" => "retrievewebpage"); 198 | 199 | if (isset($state["options"]["async"]) && $state["options"]["async"]) 200 | { 201 | $state["async"] = true; 202 | $state["httpstate"] = $result["state"]; 203 | 204 | $state["state"] = "process_async"; 205 | } 206 | else 207 | { 208 | $state["result"] = $result; 209 | 210 | $state["state"] = "post_retrieval"; 211 | } 212 | 213 | break; 214 | } 215 | case "process_async": 216 | { 217 | // Run a cycle of the HTTP state processor. 218 | $result = HTTP::ProcessState($state["httpstate"]); 219 | if (!$result["success"]) return $result; 220 | 221 | $result["url"] = $state["url"]; 222 | $result["options"] = $state["options"]; 223 | unset($result["options"]["files"]); 224 | unset($result["options"]["body"]); 225 | $result["firstreqts"] = $state["startts"]; 226 | $result["numredirects"] = $state["numredirects"]; 227 | $result["redirectts"] = $state["redirectts"]; 228 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 229 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 230 | 231 | $state["httpstate"] = false; 232 | $state["result"] = $result; 233 | 234 | $state["state"] = "post_retrieval"; 235 | 236 | break; 237 | } 238 | case "post_retrieval": 239 | { 240 | // Set up structures for another round. 241 | if ($this->data["autoreferer"]) $this->data["referer"] = $state["url"]; 242 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"]) 243 | { 244 | $state["redirectts"] = microtime(true); 245 | 246 | unset($state["tempoptions"]["method"]); 247 | unset($state["tempoptions"]["write_body_callback"]); 248 | unset($state["tempoptions"]["body"]); 249 | unset($state["tempoptions"]["postvars"]); 250 | unset($state["tempoptions"]["files"]); 251 | 252 | $state["tempoptions"]["headers"]["Referer"] = $state["url"]; 253 | $state["url"] = $state["result"]["headers"]["Location"][0]; 254 | 255 | // Generate an absolute URL. 256 | if ($this->data["referer"] != "") $state["url"] = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $state["url"]); 257 | 258 | $urlinfo2 = HTTP::ExtractURL($state["url"]); 259 | 260 | if (!isset($this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) || !$this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) 261 | { 262 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed. Server attempted to redirect to '%s'.", $urlinfo2["scheme"], $state["url"]), "info" => $state["result"], "errorcode" => "allowed_redir_protocols"); 263 | } 264 | 265 | if ($urlinfo2["host"] != $state["urlinfo"]["host"]) 266 | { 267 | unset($state["tempoptions"]["headers"]["Host"]); 268 | unset($state["httpopts"]["headers"]["Host"]); 269 | 270 | unset($state["httpopts"]["headers"]["Authorization"]); 271 | unset($state["tempoptions"]["headers"]["Authorization"]); 272 | } 273 | 274 | $state["urlinfo"] = $urlinfo2; 275 | $state["numredirects"]++; 276 | } 277 | 278 | // Handle any 'Set-Cookie' headers. 279 | if (isset($state["result"]["headers"]["Set-Cookie"])) 280 | { 281 | foreach ($state["result"]["headers"]["Set-Cookie"] as $cookie) 282 | { 283 | $items = explode(";", $cookie); 284 | $item = trim(array_shift($items)); 285 | if ($item != "") 286 | { 287 | $cookie2 = array(); 288 | $pos = strpos($item, "="); 289 | if ($pos === false) 290 | { 291 | $cookie2["name"] = urldecode($item); 292 | $cookie2["value"] = ""; 293 | } 294 | else 295 | { 296 | $cookie2["name"] = urldecode(substr($item, 0, $pos)); 297 | $cookie2["value"] = urldecode(substr($item, $pos + 1)); 298 | } 299 | 300 | $cookie = array(); 301 | foreach ($items as $item) 302 | { 303 | $item = trim($item); 304 | if ($item != "") 305 | { 306 | $pos = strpos($item, "="); 307 | if ($pos === false) $cookie[strtolower(trim(urldecode($item)))] = ""; 308 | else $cookie[strtolower(trim(urldecode(substr($item, 0, $pos))))] = urldecode(substr($item, $pos + 1)); 309 | } 310 | } 311 | $cookie = array_merge($cookie, $cookie2); 312 | 313 | if (isset($cookie["expires"])) 314 | { 315 | $ts = HTTP::GetDateTimestamp($cookie["expires"]); 316 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", ($ts === false ? time() - 24 * 60 * 60 : $ts)); 317 | } 318 | else if (isset($cookie["max-age"])) 319 | { 320 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", time() + (int)$cookie["max-age"]); 321 | } 322 | else 323 | { 324 | unset($cookie["expires_ts"]); 325 | } 326 | 327 | if (!isset($cookie["domain"])) $cookie["domain"] = $state["dothost"]; 328 | if (!isset($cookie["path"])) $cookie["path"] = $state["cookiepath"]; 329 | 330 | $this->SetCookie($cookie); 331 | } 332 | } 333 | } 334 | 335 | if ($state["numfollow"] > 0) $state["numfollow"]--; 336 | 337 | // If this is a redirect, handle it by starting over. 338 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"] && $state["numfollow"]) 339 | { 340 | $state["result"] = false; 341 | 342 | $state["state"] = "initialize"; 343 | } 344 | else 345 | { 346 | $state["result"]["numredirects"] = $state["numredirects"]; 347 | $state["result"]["redirectts"] = $state["redirectts"]; 348 | 349 | // Extract the forms from the page in a parsed format. 350 | // Call WebBrowser::GenerateFormRequest() to prepare an actual request for Process(). 351 | if ($this->data["extractforms"]) $state["result"]["forms"] = $this->ExtractForms($state["result"]["url"], $state["result"]["body"], (isset($state["tempoptions"]["extractforms_hint"]) ? $state["tempoptions"]["extractforms_hint"] : false)); 352 | 353 | $state["state"] = "done"; 354 | } 355 | 356 | break; 357 | } 358 | } 359 | } 360 | 361 | return $state["result"]; 362 | } 363 | 364 | public function Process($url, $tempoptions = array()) 365 | { 366 | $startts = microtime(true); 367 | $redirectts = $startts; 368 | 369 | // Handle older function call: Process($url, $profile, $tempoptions) 370 | if (is_string($tempoptions)) 371 | { 372 | $args = func_get_args(); 373 | if (count($args) < 3) $tempoptions = array(); 374 | else $tempoptions = $args[2]; 375 | 376 | $tempoptions["profile"] = $args[1]; 377 | } 378 | 379 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 380 | 381 | if (isset($tempoptions["timeout"])) $timeout = $tempoptions["timeout"]; 382 | else if (isset($this->data["httpopts"]["timeout"])) $timeout = $this->data["httpopts"]["timeout"]; 383 | else $timeout = false; 384 | 385 | // Deal with possible application hanging issues. 386 | if (isset($tempoptions["streamtimeout"])) $streamtimeout = $tempoptions["streamtimeout"]; 387 | else if (isset($this->data["httpopts"]["streamtimeout"])) $streamtimeout = $this->data["httpopts"]["streamtimeout"]; 388 | else $streamtimeout = 300; 389 | $tempoptions["streamtimeout"] = $streamtimeout; 390 | 391 | if (!isset($this->data["httpopts"]["headers"])) $this->data["httpopts"]["headers"] = array(); 392 | $this->data["httpopts"]["headers"] = HTTP::NormalizeHeaders($this->data["httpopts"]["headers"]); 393 | unset($this->data["httpopts"]["method"]); 394 | unset($this->data["httpopts"]["write_body_callback"]); 395 | unset($this->data["httpopts"]["body"]); 396 | unset($this->data["httpopts"]["postvars"]); 397 | unset($this->data["httpopts"]["files"]); 398 | 399 | $httpopts = $this->data["httpopts"]; 400 | $numfollow = $this->data["maxfollow"]; 401 | $numredirects = 0; 402 | $totalrawsendsize = 0; 403 | 404 | if (!isset($tempoptions["headers"])) $tempoptions["headers"] = array(); 405 | $tempoptions["headers"] = HTTP::NormalizeHeaders($tempoptions["headers"]); 406 | if (isset($tempoptions["headers"]["Referer"])) $this->data["referer"] = $tempoptions["headers"]["Referer"]; 407 | 408 | // If a referrer is specified, use it to generate an absolute URL. 409 | if ($this->data["referer"] != "") $url = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $url); 410 | 411 | $urlinfo = HTTP::ExtractURL($url); 412 | 413 | // Initialize the process state array. 414 | $state = array( 415 | "async" => false, 416 | "startts" => $startts, 417 | "redirectts" => $redirectts, 418 | "timeout" => $timeout, 419 | "tempoptions" => $tempoptions, 420 | "httpopts" => $httpopts, 421 | "numfollow" => $numfollow, 422 | "numredirects" => $numredirects, 423 | "totalrawsendsize" => $totalrawsendsize, 424 | "profile" => $profile, 425 | "url" => $url, 426 | "urlinfo" => $urlinfo, 427 | 428 | "state" => "initialize", 429 | "httpstate" => false, 430 | "result" => false, 431 | ); 432 | 433 | // Run at least one state cycle to properly initialize the state array. 434 | $result = $this->ProcessState($state); 435 | 436 | // Return the state for async calls. Caller must call ProcessState(). 437 | if ($state["async"]) return array("success" => true, "state" => $state); 438 | 439 | return $result; 440 | } 441 | 442 | // Implements the correct MultiAsyncHelper responses for WebBrowser instances. 443 | public function ProcessAsync__Handler($mode, &$data, $key, &$info) 444 | { 445 | switch ($mode) 446 | { 447 | case "init": 448 | { 449 | if ($info["init"]) $data = $info["keep"]; 450 | else 451 | { 452 | $info["result"] = $this->Process($info["url"], $info["tempoptions"]); 453 | if (!$info["result"]["success"]) 454 | { 455 | $info["keep"] = false; 456 | 457 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 458 | } 459 | else 460 | { 461 | $info["state"] = $info["result"]["state"]; 462 | 463 | // Move to the live queue. 464 | $data = true; 465 | } 466 | } 467 | 468 | break; 469 | } 470 | case "update": 471 | case "read": 472 | case "write": 473 | { 474 | if ($info["keep"]) 475 | { 476 | $info["result"] = $this->ProcessState($info["state"]); 477 | if ($info["result"]["success"] || $info["result"]["errorcode"] !== "no_data") $info["keep"] = false; 478 | 479 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 480 | 481 | if ($mode === "update") $data = $info["keep"]; 482 | } 483 | 484 | break; 485 | } 486 | case "readfps": 487 | { 488 | if ($info["state"]["httpstate"] !== false && HTTP::WantRead($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 489 | 490 | break; 491 | } 492 | case "writefps": 493 | { 494 | if ($info["state"]["httpstate"] !== false && HTTP::WantWrite($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 495 | 496 | break; 497 | } 498 | case "cleanup": 499 | { 500 | // When true, caller is removing. Otherwise, detaching from the queue. 501 | if ($data === true) 502 | { 503 | if (isset($info["state"])) 504 | { 505 | if ($info["state"]["httpstate"] !== false) HTTP::ForceClose($info["state"]["httpstate"]); 506 | 507 | unset($info["state"]); 508 | } 509 | 510 | $info["keep"] = false; 511 | } 512 | 513 | break; 514 | } 515 | } 516 | } 517 | 518 | public function ProcessAsync($helper, $key, $callback, $url, $tempoptions = array()) 519 | { 520 | $tempoptions["async"] = true; 521 | 522 | // Handle older function call: ProcessAsync($helper, $key, $callback, $url, $profile, $tempoptions) 523 | if (is_string($tempoptions)) 524 | { 525 | $args = func_get_args(); 526 | if (count($args) < 6) $tempoptions = array(); 527 | else $tempoptions = $args[5]; 528 | 529 | $tempoptions["profile"] = $args[4]; 530 | } 531 | 532 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 533 | 534 | $info = array( 535 | "init" => false, 536 | "keep" => true, 537 | "callback" => $callback, 538 | "url" => $url, 539 | "tempoptions" => $tempoptions, 540 | "result" => false 541 | ); 542 | 543 | $helper->Set($key, $info, array($this, "ProcessAsync__Handler")); 544 | 545 | return array("success" => true); 546 | } 547 | 548 | public function ExtractForms($baseurl, $data, $hint = false) 549 | { 550 | $result = array(); 551 | 552 | $lasthint = ""; 553 | $hintmap = array(); 554 | if ($this->html === false) 555 | { 556 | if (!class_exists("simple_html_dom", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/simple_html_dom.php"; 557 | 558 | $this->html = new simple_html_dom(); 559 | } 560 | $this->html->load($data); 561 | $rows = $this->html->find("label[for]"); 562 | foreach ($rows as $row) 563 | { 564 | $hintmap[trim($row->for)] = trim($row->plaintext); 565 | } 566 | $html5rows = $this->html->find("input[form],textarea[form],select[form],button[form],datalist[id]" . ($hint !== false ? "," . $hint : "")); 567 | $rows = $this->html->find("form"); 568 | foreach ($rows as $row) 569 | { 570 | $info = array(); 571 | if (isset($row->id)) $info["id"] = trim($row->id); 572 | if (isset($row->name)) $info["name"] = (string)$row->name; 573 | $info["action"] = (isset($row->action) ? HTTP::ConvertRelativeToAbsoluteURL($baseurl, (string)$row->action) : $baseurl); 574 | $info["method"] = (isset($row->method) && strtolower(trim($row->method)) == "post" ? "post" : "get"); 575 | if ($info["method"] == "post") $info["enctype"] = (isset($row->enctype) ? strtolower($row->enctype) : "application/x-www-form-urlencoded"); 576 | if (isset($row->{"accept-charset"})) $info["accept-charset"] = (string)$row->{"accept-charset"}; 577 | 578 | $fields = array(); 579 | $rows2 = $row->find("input,textarea,select,button" . ($hint !== false ? "," . $hint : "")); 580 | foreach ($rows2 as $row2) 581 | { 582 | if (!isset($row2->form)) 583 | { 584 | if (isset($row2->id) && $row2->id != "" && isset($hintmap[trim($row2->id)])) $lasthint = $hintmap[trim($row2->id)]; 585 | 586 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 587 | } 588 | } 589 | 590 | // Handle HTML5. 591 | if (isset($info["id"]) && $info["id"] != "") 592 | { 593 | foreach ($html5rows as $row2) 594 | { 595 | if (strpos(" " . $info["id"] . " ", " " . $row2->form . " ") !== false) 596 | { 597 | if (isset($hintmap[$info["id"]])) $lasthint = $hintmap[$info["id"]]; 598 | 599 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 600 | } 601 | } 602 | } 603 | 604 | $form = new WebBrowserForm(); 605 | $form->info = $info; 606 | $form->fields = $fields; 607 | $result[] = $form; 608 | } 609 | 610 | return $result; 611 | } 612 | 613 | private function ExtractFieldFromDOM(&$fields, $row, &$lasthint) 614 | { 615 | switch ($row->tag) 616 | { 617 | case "input": 618 | { 619 | if (!isset($row->name) && ($row->type === "submit" || $row->type === "image")) $row->name = ""; 620 | 621 | if (isset($row->name) && is_string($row->name)) 622 | { 623 | $field = array( 624 | "id" => (isset($row->id) ? (string)$row->id : false), 625 | "type" => "input." . (isset($row->type) ? strtolower($row->type) : "text"), 626 | "name" => $row->name, 627 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 628 | ); 629 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") 630 | { 631 | $field["checked"] = (isset($row->checked)); 632 | 633 | if ($field["value"] === "") $field["value"] = "on"; 634 | } 635 | 636 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 637 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image") $field["hint"] = $field["type"] . "|" . $field["value"]; 638 | else if ($lasthint !== "") $field["hint"] = $lasthint; 639 | 640 | $fields[] = $field; 641 | 642 | $lasthint = ""; 643 | } 644 | 645 | break; 646 | } 647 | case "textarea": 648 | { 649 | if (isset($row->name) && is_string($row->name)) 650 | { 651 | $field = array( 652 | "id" => (isset($row->id) ? (string)$row->id : false), 653 | "type" => "textarea", 654 | "name" => $row->name, 655 | "value" => html_entity_decode($row->innertext, ENT_COMPAT, "UTF-8") 656 | ); 657 | 658 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 659 | else if ($lasthint !== "") $field["hint"] = $lasthint; 660 | 661 | $fields[] = $field; 662 | 663 | $lasthint = ""; 664 | } 665 | 666 | break; 667 | } 668 | case "select": 669 | { 670 | if (isset($row->name) && is_string($row->name)) 671 | { 672 | if (isset($row->multiple)) 673 | { 674 | // Change the type into multiple checkboxes. 675 | $rows = $row->find("option"); 676 | foreach ($rows as $row2) 677 | { 678 | $field = array( 679 | "id" => (isset($row->id) ? (string)$row->id : false), 680 | "type" => "input.checkbox", 681 | "name" => $row->name, 682 | "value" => (isset($row2->value) ? html_entity_decode($row2->value, ENT_COMPAT, "UTF-8") : ""), 683 | "display" => (string)$row2->innertext 684 | ); 685 | if ($lasthint !== "") $field["hint"] = $lasthint; 686 | 687 | $fields[] = $field; 688 | } 689 | } 690 | else 691 | { 692 | $val = false; 693 | $options = array(); 694 | $rows = $row->find("option"); 695 | foreach ($rows as $row2) 696 | { 697 | $options[$row2->value] = (string)$row2->innertext; 698 | 699 | if ($val === false && isset($row2->selected)) $val = html_entity_decode($row2->value, ENT_COMPAT, "UTF-8"); 700 | } 701 | if ($val === false && count($options)) 702 | { 703 | $val = array_keys($options); 704 | $val = $val[0]; 705 | } 706 | if ($val === false) $val = ""; 707 | 708 | $field = array( 709 | "id" => (isset($row->id) ? (string)$row->id : false), 710 | "type" => "select", 711 | "name" => $row->name, 712 | "value" => $val, 713 | "options" => $options 714 | ); 715 | if ($lasthint !== "") $field["hint"] = $lasthint; 716 | 717 | $fields[] = $field; 718 | } 719 | 720 | $lasthint = ""; 721 | } 722 | 723 | break; 724 | } 725 | case "button": 726 | { 727 | if (isset($row->name) && is_string($row->name)) 728 | { 729 | $field = array( 730 | "id" => (isset($row->id) ? (string)$row->id : false), 731 | "type" => "button." . (isset($row->type) ? strtolower($row->type) : "submit"), 732 | "name" => $row->name, 733 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 734 | ); 735 | $field["hint"] = (trim($row->plaintext) !== "" ? trim($row->plaintext) : "button|" . $field["value"]); 736 | 737 | $fields[] = $field; 738 | 739 | $lasthint = ""; 740 | } 741 | 742 | break; 743 | } 744 | case "datalist": 745 | { 746 | // Do nothing since browsers don't actually enforce this tag's values. 747 | 748 | break; 749 | } 750 | default: 751 | { 752 | // Hint for the next element. 753 | $lasthint = (string)$row->plaintext; 754 | 755 | break; 756 | } 757 | } 758 | } 759 | 760 | public static function InteractiveFormFill($forms, $showselected = false) 761 | { 762 | if (!is_array($forms)) $forms = array($forms); 763 | 764 | if (!count($forms)) return false; 765 | 766 | if (count($forms) == 1) $form = reset($forms); 767 | else 768 | { 769 | echo self::WBTranslate("There are multiple forms available to fill out:\n"); 770 | foreach ($forms as $num => $form) 771 | { 772 | echo self::WBTranslate("\t%d:\n", $num + 1); 773 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t\t%s: %s\n", $key, $val); 774 | echo self::WBTranslate("\t\tfields: %d\n", count($form->GetVisibleFields(false))); 775 | echo self::WBTranslate("\t\tbuttons: %d\n", count($form->GetVisibleFields(true)) - count($form->GetVisibleFields(false))); 776 | echo "\n"; 777 | } 778 | 779 | do 780 | { 781 | echo self::WBTranslate("Select: "); 782 | 783 | $num = (int)trim(fgets(STDIN)) - 1; 784 | } while (!isset($forms[$num])); 785 | 786 | $form = $forms[$num]; 787 | } 788 | 789 | if ($showselected) 790 | { 791 | echo self::WBTranslate("Selected form:\n"); 792 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t%s: %s\n", $key, $val); 793 | echo "\n"; 794 | } 795 | 796 | if (count($form->GetVisibleFields(false))) 797 | { 798 | echo self::WBTranslate("Select form fields by field number to edit a field. When ready to submit the form, leave 'Field number' empty.\n\n"); 799 | 800 | do 801 | { 802 | echo self::WBTranslate("Editable form fields:\n"); 803 | foreach ($form->fields as $num => $field) 804 | { 805 | if ($field["type"] == "input.hidden" || $field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button.") continue; 806 | 807 | echo self::WBTranslate("\t%d: %s - %s\n", $num + 1, $field["name"], (is_array($field["value"]) ? json_encode($field["value"], JSON_PRETTY_PRINT) : $field["value"]) . (($field["type"] == "input.radio" || $field["type"] == "input.checkbox") ? ($field["checked"] ? self::WBTranslate(" [Y]") : self::WBTranslate(" [N]")) : "") . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 808 | } 809 | echo "\n"; 810 | 811 | do 812 | { 813 | echo self::WBTranslate("Field number: "); 814 | 815 | $num = trim(fgets(STDIN)); 816 | if ($num === "") break; 817 | 818 | $num = (int)$num - 1; 819 | } while (!isset($form->fields[$num]) || $form->fields[$num]["type"] == "input.hidden" || $form->fields[$num]["type"] == "input.submit" || $form->fields[$num]["type"] == "input.image" || $form->fields[$num]["type"] == "input.button" || substr($form->fields[$num]["type"], 0, 7) == "button."); 820 | 821 | if ($num === "") 822 | { 823 | echo "\n"; 824 | 825 | break; 826 | } 827 | 828 | $field = $form->fields[$num]; 829 | $prefix = (isset($field["hint"]) && $field["hint"] !== "" ? $field["hint"] . " | " : "") . $field["name"]; 830 | 831 | if ($field["type"] == "select") 832 | { 833 | echo self::WBTranslate("[%s] Options:\n", $prefix); 834 | foreach ($field["options"] as $key => $val) 835 | { 836 | echo self::WBTranslate("\t%s: %s\n"); 837 | } 838 | 839 | do 840 | { 841 | echo self::WBTranslate("[%s] Select: ", $prefix); 842 | 843 | $select = rtrim(fgets(STDIN)); 844 | } while (!isset($field["options"][$select])); 845 | 846 | $form->fields[$num]["value"] = $select; 847 | } 848 | else if ($field["type"] == "input.radio") 849 | { 850 | $form->SetFormValue($field["name"], $field["value"], true, "input.radio"); 851 | } 852 | else if ($field["type"] == "input.checkbox") 853 | { 854 | $form->fields[$num]["checked"] = !$field["checked"]; 855 | } 856 | else if ($field["type"] == "input.file") 857 | { 858 | do 859 | { 860 | echo self::WBTranslate("[%s] Filename: ", $prefix); 861 | 862 | $filename = rtrim(fgets(STDIN)); 863 | } while ($filename !== "" && !file_exists($filename)); 864 | 865 | if ($filename === "") $form->fields[$num]["value"] = ""; 866 | else 867 | { 868 | $form->fields[$num]["value"] = array( 869 | "filename" => $filename, 870 | "type" => "application/octet-stream", 871 | "datafile" => $filename 872 | ); 873 | } 874 | } 875 | else 876 | { 877 | echo self::WBTranslate("[%s] New value: ", $prefix); 878 | 879 | $form->fields[$num]["value"] = rtrim(fgets(STDIN)); 880 | } 881 | 882 | echo "\n"; 883 | 884 | } while (1); 885 | } 886 | 887 | $submitoptions = array(array("name" => self::WBTranslate("Default action"), "value" => self::WBTranslate("Might not work"), "hint" => "Default action")); 888 | foreach ($form->fields as $num => $field) 889 | { 890 | if ($field["type"] != "input.submit" && $field["type"] != "input.image" && $field["type"] != "input.button" && $field["type"] != "button.submit") continue; 891 | 892 | $submitoptions[] = $field; 893 | } 894 | 895 | if (count($submitoptions) <= 2) $num = count($submitoptions) - 1; 896 | else 897 | { 898 | echo self::WBTranslate("Available submit buttons:\n"); 899 | foreach ($submitoptions as $num => $field) 900 | { 901 | echo self::WBTranslate("\t%d: %s - %s\n", $num, $field["name"], $field["value"] . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 902 | } 903 | echo "\n"; 904 | 905 | do 906 | { 907 | echo self::WBTranslate("Select: "); 908 | 909 | $num = (int)fgets(STDIN); 910 | } while (!isset($submitoptions[$num])); 911 | 912 | echo "\n"; 913 | } 914 | 915 | $result = $form->GenerateFormRequest(($num ? $submitoptions[$num]["name"] : false), ($num ? $submitoptions[$num]["value"] : false)); 916 | 917 | return $result; 918 | } 919 | 920 | public function GetCookies() 921 | { 922 | return $this->data["cookies"]; 923 | } 924 | 925 | public function SetCookie($cookie) 926 | { 927 | if (!isset($cookie["domain"]) || !isset($cookie["path"]) || !isset($cookie["name"]) || !isset($cookie["value"])) return array("success" => false, "error" => self::WBTranslate("SetCookie() requires 'domain', 'path', 'name', and 'value' to be options."), "errorcode" => "missing_information"); 928 | 929 | $cookie["domain"] = strtolower($cookie["domain"]); 930 | if (substr($cookie["domain"], 0, 1) != ".") $cookie["domain"] = "." . $cookie["domain"]; 931 | 932 | $cookie["path"] = str_replace("\\", "/", $cookie["path"]); 933 | if (substr($cookie["path"], -1) != "/") $cookie["path"] = "/"; 934 | 935 | if (!isset($this->data["cookies"][$cookie["domain"]])) $this->data["cookies"][$cookie["domain"]] = array(); 936 | if (!isset($this->data["cookies"][$cookie["domain"]][$cookie["path"]])) $this->data["cookies"][$cookie["domain"]][$cookie["path"]] = array(); 937 | $this->data["cookies"][$cookie["domain"]][$cookie["path"]][$cookie["name"]] = $cookie; 938 | 939 | return array("success" => true); 940 | } 941 | 942 | // Simulates closing a web browser. 943 | public function DeleteSessionCookies() 944 | { 945 | foreach ($this->data["cookies"] as $domain => $paths) 946 | { 947 | foreach ($paths as $path => $cookies) 948 | { 949 | foreach ($cookies as $num => $info) 950 | { 951 | if (!isset($info["expires_ts"])) unset($this->data["cookies"][$domain][$path][$num]); 952 | } 953 | 954 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 955 | } 956 | 957 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 958 | } 959 | } 960 | 961 | public function DeleteCookies($domainpattern, $pathpattern, $namepattern) 962 | { 963 | foreach ($this->data["cookies"] as $domain => $paths) 964 | { 965 | if ($domainpattern == "" || substr($domain, -strlen($domainpattern)) == $domainpattern) 966 | { 967 | foreach ($paths as $path => $cookies) 968 | { 969 | if ($pathpattern == "" || substr($path, 0, strlen($pathpattern)) == $pathpattern) 970 | { 971 | foreach ($cookies as $num => $info) 972 | { 973 | if ($namepattern == "" || strpos($info["name"], $namepattern) !== false) unset($this->data["cookies"][$domain][$path][$num]); 974 | } 975 | 976 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 977 | } 978 | } 979 | 980 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 981 | } 982 | } 983 | } 984 | 985 | private function GetExpiresTimestamp($ts) 986 | { 987 | $year = (int)substr($ts, 0, 4); 988 | $month = (int)substr($ts, 5, 2); 989 | $day = (int)substr($ts, 8, 2); 990 | $hour = (int)substr($ts, 11, 2); 991 | $min = (int)substr($ts, 14, 2); 992 | $sec = (int)substr($ts, 17, 2); 993 | 994 | return gmmktime($hour, $min, $sec, $month, $day, $year); 995 | } 996 | 997 | public static function WBTranslate() 998 | { 999 | $args = func_get_args(); 1000 | if (!count($args)) return ""; 1001 | 1002 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 1003 | } 1004 | } 1005 | 1006 | class WebBrowserForm 1007 | { 1008 | public $info, $fields; 1009 | 1010 | public function __construct() 1011 | { 1012 | $this->info = array(); 1013 | $this->fields = array(); 1014 | } 1015 | 1016 | public function FindFormFields($name = false, $value = false, $type = false) 1017 | { 1018 | $fields = array(); 1019 | foreach ($this->fields as $num => $field) 1020 | { 1021 | if (($type === false || $field["type"] === $type) && ($name === false || $field["name"] === $name) && ($value === false || $field["value"] === $value)) 1022 | { 1023 | $fields[] = $field; 1024 | } 1025 | } 1026 | 1027 | return $fields; 1028 | } 1029 | 1030 | public function GetHintMap() 1031 | { 1032 | $result = array(); 1033 | foreach ($this->fields as $num => $field) 1034 | { 1035 | if (isset($field["hint"])) $result[$field["hint"]] = $field["name"]; 1036 | } 1037 | 1038 | return $result; 1039 | } 1040 | 1041 | public function GetVisibleFields($submit) 1042 | { 1043 | $result = array(); 1044 | foreach ($this->fields as $num => $field) 1045 | { 1046 | if ($field["type"] == "input.hidden" || (!$submit && ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button."))) continue; 1047 | 1048 | $result[$num] = $field; 1049 | } 1050 | 1051 | return $result; 1052 | } 1053 | 1054 | public function GetFormValue($name, $checkval = false, $type = false) 1055 | { 1056 | $val = false; 1057 | foreach ($this->fields as $field) 1058 | { 1059 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1060 | { 1061 | if (is_string($checkval)) 1062 | { 1063 | if ($checkval === $field["value"]) 1064 | { 1065 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") $val = $field["checked"]; 1066 | else $val = $field["value"]; 1067 | } 1068 | } 1069 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1070 | { 1071 | $val = $field["value"]; 1072 | } 1073 | } 1074 | } 1075 | 1076 | return $val; 1077 | } 1078 | 1079 | public function SetFormValue($name, $value, $checked = false, $type = false, $create = false) 1080 | { 1081 | $result = false; 1082 | foreach ($this->fields as $num => $field) 1083 | { 1084 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1085 | { 1086 | if ($field["type"] == "input.radio") 1087 | { 1088 | $this->fields[$num]["checked"] = ($field["value"] === $value ? $checked : false); 1089 | $result = true; 1090 | } 1091 | else if ($field["type"] == "input.checkbox") 1092 | { 1093 | if ($field["value"] === $value) $this->fields[$num]["checked"] = $checked; 1094 | $result = true; 1095 | } 1096 | else if ($field["type"] != "select" || !isset($field["options"]) || isset($field["options"][$value])) 1097 | { 1098 | $this->fields[$num]["value"] = $value; 1099 | $result = true; 1100 | } 1101 | } 1102 | } 1103 | 1104 | // Add the field if it doesn't exist. 1105 | if (!$result && $create) 1106 | { 1107 | $this->fields[] = array( 1108 | "id" => false, 1109 | "type" => ($type !== false ? $type : "input.text"), 1110 | "name" => $name, 1111 | "value" => $value, 1112 | "checked" => $checked 1113 | ); 1114 | } 1115 | 1116 | return $result; 1117 | } 1118 | 1119 | public function GenerateFormRequest($submitname = false, $submitvalue = false) 1120 | { 1121 | $method = $this->info["method"]; 1122 | $fields = array(); 1123 | $files = array(); 1124 | foreach ($this->fields as $field) 1125 | { 1126 | if ($field["type"] == "input.file") 1127 | { 1128 | if (is_array($field["value"])) 1129 | { 1130 | $field["value"]["name"] = $field["name"]; 1131 | $files[] = $field["value"]; 1132 | $method = "post"; 1133 | } 1134 | } 1135 | else if ($field["type"] == "input.reset" || $field["type"] == "button.reset") 1136 | { 1137 | } 1138 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "button.submit") 1139 | { 1140 | if (($submitname === false || $field["name"] === $submitname) && ($submitvalue === false || $field["value"] === $submitvalue)) 1141 | { 1142 | if ($submitname !== "") 1143 | { 1144 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1145 | $fields[$field["name"]][] = $field["value"]; 1146 | } 1147 | 1148 | if ($field["type"] == "input.image") 1149 | { 1150 | if (!isset($fields["x"])) $fields["x"] = array(); 1151 | $fields["x"][] = "1"; 1152 | 1153 | if (!isset($fields["y"])) $fields["y"] = array(); 1154 | $fields["y"][] = "1"; 1155 | } 1156 | } 1157 | } 1158 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1159 | { 1160 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1161 | $fields[$field["name"]][] = $field["value"]; 1162 | } 1163 | } 1164 | 1165 | if ($method == "get") 1166 | { 1167 | $url = HTTP::ExtractURL($this->info["action"]); 1168 | unset($url["query"]); 1169 | $url["queryvars"] = $fields; 1170 | $result = array( 1171 | "url" => HTTP::CondenseURL($url), 1172 | "options" => array() 1173 | ); 1174 | } 1175 | else 1176 | { 1177 | $result = array( 1178 | "url" => $this->info["action"], 1179 | "options" => array( 1180 | "postvars" => $fields, 1181 | "files" => $files 1182 | ) 1183 | ); 1184 | } 1185 | 1186 | return $result; 1187 | } 1188 | } 1189 | ?> -------------------------------------------------------------------------------- /server_exts/scripts.php: -------------------------------------------------------------------------------- 1 | true, "info" => array("guests" => $guests)); 25 | } 26 | 27 | public function RegisterHandlers($em) 28 | { 29 | } 30 | 31 | public function InitServer() 32 | { 33 | $ignore = array( 34 | "PHP_SELF" => true, 35 | "SCRIPT_NAME" => true, 36 | "SCRIPT_FILENAME" => true, 37 | "PATH_TRANSLATED" => true, 38 | "DOCUMENT_ROOT" => true, 39 | "REQUEST_TIME_FLOAT" => true, 40 | "REQUEST_TIME" => true, 41 | "argv" => true, 42 | "argc" => true, 43 | ); 44 | 45 | $this->baseenv = array(); 46 | foreach ($_SERVER as $key => $val) 47 | { 48 | if (!isset($ignore[$key]) && is_string($val)) $this->baseenv[$key] = $val; 49 | } 50 | 51 | $this->exectabs = array(); 52 | $this->exectabsts = array(); 53 | $this->runqueue = array(); 54 | $this->running = array(); 55 | $this->idmap = array(); 56 | $this->monitors = array(); 57 | $this->usercache = array(); 58 | $this->groupcache = array(); 59 | } 60 | 61 | private function GetUserInfoByID($uid) 62 | { 63 | if (!function_exists("posix_getpwuid")) return false; 64 | 65 | if (!isset($this->usercache[$uid])) 66 | { 67 | $user = @posix_getpwuid($uid); 68 | if ($user === false || !is_array($user)) $this->usercache[$uid] = false; 69 | else 70 | { 71 | $this->usercache[$uid] = $user; 72 | $this->usercache["_" . $user["name"]] = $user; 73 | } 74 | } 75 | 76 | return $this->usercache[$uid]; 77 | } 78 | 79 | private function GetUserInfoByName($name) 80 | { 81 | if (!function_exists("posix_getpwnam")) return false; 82 | 83 | if (!isset($this->usercache["_" . $name])) 84 | { 85 | $user = @posix_getpwnam($name); 86 | if ($user === false || !is_array($user)) $this->usercache["_" . $name] = false; 87 | else 88 | { 89 | $this->usercache[$user["uid"]] = $user; 90 | $this->usercache["_" . $name] = $user; 91 | } 92 | } 93 | 94 | return $this->usercache["_" . $name]; 95 | } 96 | 97 | private function GetUserName($uid) 98 | { 99 | $user = $this->GetUserInfoByID($uid); 100 | 101 | return ($user !== false ? $user["name"] : ""); 102 | } 103 | 104 | private function GetGroupInfoByID($gid) 105 | { 106 | if (!function_exists("posix_getgrgid")) return false; 107 | 108 | if (!isset($this->groupcache[$gid])) 109 | { 110 | $group = @posix_getgrgid($gid); 111 | if ($group === false || !is_array($group)) $this->groupcache[$gid] = ""; 112 | else 113 | { 114 | $this->groupcache[$gid] = $group; 115 | $this->groupcache["_" . $group["name"]] = $group; 116 | } 117 | } 118 | 119 | return $this->groupcache[$gid]; 120 | } 121 | 122 | private function GetGroupInfoByName($name) 123 | { 124 | if (!function_exists("posix_getgrnam")) return false; 125 | 126 | if (!isset($this->groupcache["_" . $name])) 127 | { 128 | $group = @posix_getgrnam($name); 129 | if ($group === false || !is_array($group)) $this->groupcache["_" . $name] = ""; 130 | else 131 | { 132 | $this->groupcache[$group["gid"]] = $group; 133 | $this->groupcache["_" . $name] = $group; 134 | } 135 | } 136 | 137 | return $this->groupcache["_" . $name]; 138 | } 139 | 140 | private function GetGroupName($gid) 141 | { 142 | $group = $this->GetGroupInfoByID($gid); 143 | 144 | return ($group !== false ? $group["name"] : ""); 145 | } 146 | 147 | private function GetQueuedStatusResult($id, $name, &$info) 148 | { 149 | return array("success" => true, "id" => $id, "name" => $name, "state" => "queued", "queued" => $info["queued"], "position" => $info["queuepos"]); 150 | } 151 | 152 | private function GetRunningStatusResult($id, $name, &$info) 153 | { 154 | return array("success" => true, "id" => $id, "name" => $name, "state" => "running", "task" => $info["task"], "tasknum" => $info["tasknum"], "maxtasks" => $info["maxtasks"], "taskstart" => $info["taskstart"], "subtask" => $info["subtask"], "subtaskpercent" => $info["subtaskpercent"]); 155 | } 156 | 157 | private function GetFinalStatusResult($row) 158 | { 159 | $info = @json_decode($row->info, true); 160 | 161 | return array("success" => true, "id" => $row->id, "name" => $row->script, "state" => ($row->finished > 0 ? "done" : "incomplete_log"), "started" => (double)$row->started, "finished" => (double)$row->finished, "args" => $info["args"], "first" => (!count($info["tasks"]) ? $info["first"] : ""), "last" => (!count($info["tasks"]) ? $info["last"] : ""), "tasks" => $info["tasks"], "removelog" => $info["args"]["opts"]["removelog"]); 162 | } 163 | 164 | private function HasMonitor($uid, $name) 165 | { 166 | return (isset($this->monitors[$uid]) && (isset($this->monitors[$uid][$name]) || isset($this->monitors[$uid][""]))); 167 | } 168 | 169 | private function NotifyMonitors($uid, $name, $result) 170 | { 171 | global $wsserver; 172 | 173 | if (isset($this->monitors[$uid]) && isset($this->monitors[$uid][$name])) 174 | { 175 | foreach ($this->monitors[$uid][$name] as $wsid => $api_sequence) 176 | { 177 | $client = $wsserver->GetClient($wsid); 178 | if ($client === false) 179 | { 180 | unset($this->monitors[$uid][$name][$wsid]); 181 | if (!count($this->monitors[$uid][$name])) 182 | { 183 | unset($this->monitors[$uid][$name]); 184 | if (!count($this->monitors[$uid])) unset($this->monitors[$uid]); 185 | } 186 | } 187 | else 188 | { 189 | $result["api_sequence"] = $api_sequence; 190 | 191 | $client->websocket->Write(json_encode($result), WebSocket::FRAMETYPE_TEXT); 192 | } 193 | } 194 | } 195 | } 196 | 197 | private function FinalizeProcess($uid, $name, $id, $db, &$info) 198 | { 199 | if ($info["proc"] !== false) 200 | { 201 | foreach ($info["pipes"] as $fp) fclose($fp); 202 | 203 | proc_close($info["proc"]); 204 | } 205 | 206 | @unlink($info["basedir"] . "/status/" . $id . ".json"); 207 | 208 | if ($db !== false) 209 | { 210 | try 211 | { 212 | $row = $db->GetRow("SELECT", array( 213 | "*", 214 | "FROM" => "?", 215 | "WHERE" => "id = ?", 216 | ), "log", $id); 217 | 218 | if (!$row) 219 | { 220 | CSS_DisplayError("Unable to locate a log entry with the ID '" . $id . "' for user '" . $uid . "'.", false, false); 221 | 222 | return; 223 | } 224 | 225 | $result = $this->GetFinalStatusResult($row); 226 | $result["name"] = $name; 227 | 228 | $this->NotifyMonitors($uid, $name, $result); 229 | $this->NotifyMonitors($uid, "", $result); 230 | 231 | if ($result["removelog"]) $db->Query("DELETE", array("log", "WHERE" => "id = ?"), array($id)); 232 | } 233 | catch (Exception $e) 234 | { 235 | CSS_DisplayError("A database query failed while retrieving ID '" . $id . "' for user '" . $uid . "'.", false, false); 236 | } 237 | } 238 | 239 | unset($this->idmap[$uid][$id]); 240 | if (!count($this->idmap[$uid])) unset($this->idmap[$uid]); 241 | } 242 | 243 | private function ProcessStartFailed($info, $msg, $db, $uid, $id) 244 | { 245 | $info["loginfo"]["first"] = "[ERROR] " . $msg; 246 | $info["loginfo"]["last"] = "[ERROR] " . $msg; 247 | 248 | if ($db !== false) 249 | { 250 | try 251 | { 252 | $db->Query("UPDATE", array("log", array( 253 | "finished" => microtime(true), 254 | "info" => json_encode($info["loginfo"], JSON_UNESCAPED_SLASHES) 255 | ), "WHERE" => "id = ?"), $id); 256 | } 257 | catch (Exception $e) 258 | { 259 | CSS_DisplayError("A database query failed while updating the process log.", false, false); 260 | } 261 | } 262 | 263 | unset($this->idmap[$uid][$id]); 264 | if (!count($this->idmap[$uid])) unset($this->idmap[$uid]); 265 | } 266 | 267 | private function RemoveRunQueueEntry($uid, $name, $id) 268 | { 269 | unset($this->runqueue[$uid][$name][$id]); 270 | if (!count($this->runqueue[$uid][$name])) 271 | { 272 | unset($this->runqueue[$uid][$name]); 273 | if (!count($this->runqueue[$uid])) unset($this->runqueue[$uid]); 274 | } 275 | else 276 | { 277 | $num = 0; 278 | foreach ($this->runqueue[$uid][$name] as $id2 => $info2) 279 | { 280 | $this->runqueue[$uid][$name][$id2]["queuepos"] = $num; 281 | $info2["queuepos"] = $num; 282 | 283 | @file_put_contents($info2["basedir"] . "/status/" . $id2 . ".json", json_encode($this->GetQueuedStatusResult($id2, $name, $info2), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 284 | 285 | $num++; 286 | } 287 | } 288 | } 289 | 290 | private function UpdateRunningScripts() 291 | { 292 | global $rootpath; 293 | 294 | // Process running scripts first. 295 | foreach ($this->running as $uid => $names) 296 | { 297 | foreach ($names as $name => $idsinfo) 298 | { 299 | foreach ($idsinfo as $id => $info) 300 | { 301 | // Process stdin. 302 | if (isset($info["pipes"][0])) 303 | { 304 | if ($info["stdin"] !== "") 305 | { 306 | $result = @fwrite($info["pipes"][0], $info["stdin"]); 307 | if ($result === false) 308 | { 309 | @fclose($info["pipes"][0]); 310 | unset($info["pipes"][0]); 311 | 312 | $this->running[$uid][$name][$id]["stdin"] = ""; 313 | } 314 | else if ($result > 0) 315 | { 316 | $this->running[$uid][$name][$id]["stdin"] = (string)substr($info["stdin"], $result); 317 | } 318 | } 319 | else 320 | { 321 | @fclose($info["pipes"][0]); 322 | unset($this->running[$uid][$name][$id]["pipes"][0]); 323 | } 324 | } 325 | 326 | // Process stdout/stderr. 327 | for ($x = 1; $x < 3; $x++) 328 | { 329 | if (isset($info["pipes"][$x])) 330 | { 331 | $line = @fgets($info["pipes"][$x]); 332 | if ($line === false) 333 | { 334 | if (feof($info["pipes"][$x])) 335 | { 336 | @fclose($info["pipes"][$x]); 337 | unset($this->running[$uid][$name][$id]["pipes"][$x]); 338 | } 339 | } 340 | else if ($line !== "") 341 | { 342 | $line = trim($line); 343 | if ($info["loginfo"]["first"] === false) $this->running[$uid][$name][$id]["loginfo"]["first"] = substr($line, 0, 10000); 344 | $this->running[$uid][$name][$id]["loginfo"]["last"] = substr($line, 0, 10000); 345 | 346 | if ($line[0] === "[") 347 | { 348 | $pos = strpos($line, "]"); 349 | if ($pos !== false) 350 | { 351 | $task = (string)substr($line, 1, $pos - 1); 352 | if ($task !== "") 353 | { 354 | $line = ltrim(substr($line, $pos + 1)); 355 | 356 | if (substr($task, -1) === "%") 357 | { 358 | // Subtask percentage complete. 359 | $this->running[$uid][$name][$id]["subtask"] = $line; 360 | $this->running[$uid][$name][$id]["subtaskpercent"] = (int)$task; 361 | } 362 | else 363 | { 364 | // Main task changed. 365 | $pos = strpos($task, "/"); 366 | if ($pos !== false) 367 | { 368 | $maxtasks = (int)substr($task, $pos + 1); 369 | $task = substr($task, 0, $pos); 370 | if ($info["maxtasks"] < $maxtasks) $this->running[$uid][$name][$id]["maxtasks"] = $maxtasks; 371 | } 372 | 373 | $tasknum = (int)$task - 1; 374 | if ($info["task"] === false || $info["tasknum"] < $tasknum) $this->running[$uid][$name][$id]["taskstart"] = microtime(true); 375 | if ($info["tasknum"] < $tasknum) 376 | { 377 | if ($info["task"] !== false) 378 | { 379 | $this->running[$uid][$name][$id]["loginfo"]["tasks"][] = array( 380 | "task" => $info["task"], 381 | "start" => $info["taskstart"], 382 | "end" => microtime(true) 383 | ); 384 | } 385 | 386 | $this->running[$uid][$name][$id]["tasknum"] = $tasknum; 387 | if ($this->running[$uid][$name][$id]["maxtasks"] <= $tasknum) $this->running[$uid][$name][$id]["maxtasks"] = $tasknum + 1; 388 | $this->running[$uid][$name][$id]["subtask"] = false; 389 | $this->running[$uid][$name][$id]["subtaskpercent"] = 0; 390 | } 391 | 392 | if ($info["tasknum"] <= $tasknum) $this->running[$uid][$name][$id]["task"] = $line; 393 | 394 | $info = $this->running[$uid][$name][$id]; 395 | } 396 | 397 | @file_put_contents($info["basedir"] . "/status/" . $id . ".json", json_encode($this->GetRunningStatusResult($id, $name, $this->running[$uid][$name][$id]), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 398 | } 399 | } 400 | } 401 | } 402 | } 403 | } 404 | 405 | // If all handles have been closed, assume the process has been terminated or will be very shortly and remove this process from the running queue. 406 | if (!count($this->running[$uid][$name][$id]["pipes"])) 407 | { 408 | if ($info["task"] !== false) 409 | { 410 | $this->running[$uid][$name][$id]["loginfo"]["tasks"][] = array( 411 | "task" => $info["task"], 412 | "start" => $info["taskstart"], 413 | "end" => microtime(true) 414 | ); 415 | } 416 | 417 | // Connect to the database. 418 | $result = self::GetUserScriptsDB($info["basedir"]); 419 | $db = ($result["success"] ? $result["db"] : false); 420 | 421 | if ($db !== false) 422 | { 423 | try 424 | { 425 | $ts = microtime(true); 426 | 427 | $db->Query("UPDATE", array("log", array( 428 | "finished" => $ts, 429 | "info" => json_encode($this->running[$uid][$name][$id]["loginfo"], JSON_UNESCAPED_SLASHES) 430 | ), array( 431 | "duration" => $ts . " - started" 432 | ), "WHERE" => "id = ?"), $id); 433 | } 434 | catch (Exception $e) 435 | { 436 | CSS_DisplayError("A database query failed while updating the process log.", false, false); 437 | } 438 | } 439 | 440 | $this->FinalizeProcess($uid, $name, $id, $db, $this->running[$uid][$name][$id]); 441 | 442 | unset($this->running[$uid][$name][$id]); 443 | if (!count($this->running[$uid][$name])) 444 | { 445 | unset($this->running[$uid][$name]); 446 | if (!count($this->running[$uid])) unset($this->running[$uid]); 447 | } 448 | } 449 | } 450 | } 451 | } 452 | 453 | // Start queued scripts up to the maximum simultaneous allowed. 454 | foreach ($this->runqueue as $uid => $names) 455 | { 456 | foreach ($names as $name => $idsinfo) 457 | { 458 | if (!isset($this->running[$uid]) || !isset($this->running[$uid][$name]) || !isset($this->exectabs[$uid][$name]) || count($this->running[$uid][$name]) < $this->exectabs[$uid][$name]["opts"]["simultaneous"]) 459 | { 460 | foreach ($idsinfo as $id => $info) 461 | { 462 | if (isset($this->running[$uid]) && isset($this->running[$uid][$name]) && isset($this->exectabs[$uid][$name]) && count($this->running[$uid][$name]) >= $this->exectabs[$uid][$name]["opts"]["simultaneous"]) break; 463 | 464 | // Skip future run items. 465 | if ($info["queued"] > time()) break; 466 | 467 | // Remove this entry from the run queue. 468 | $this->RemoveRunQueueEntry($uid, $name, $id); 469 | 470 | // Connect to the database. 471 | $result = self::GetUserScriptsDB($info["basedir"]); 472 | $db = ($result["success"] ? $result["db"] : false); 473 | 474 | if (!count($info["args"]["params"]) || (isset($this->exectabs[$uid][$name]) && $this->exectabs[$uid][$name]["opts"]["noexec"])) 475 | { 476 | // No process. Just finalize the log entry and notify any monitors. 477 | if ($db !== false) 478 | { 479 | $info["loginfo"]["first"] = ""; 480 | $info["loginfo"]["last"] = ""; 481 | 482 | try 483 | { 484 | $ts = microtime(true); 485 | 486 | $db->Query("UPDATE", array("log", array( 487 | "started" => $ts, 488 | "finished" => $ts, 489 | "info" => $info["loginfo"] 490 | ), "WHERE" => "id = ?"), $id); 491 | } 492 | catch (Exception $e) 493 | { 494 | CSS_DisplayError("A database query failed while updating the process log.", false, false); 495 | } 496 | 497 | $this->FinalizeProcess($uid, $name, $id, $db, $info); 498 | } 499 | } 500 | else 501 | { 502 | // Set up the process environment. 503 | $env = $this->baseenv; 504 | foreach ($info["args"]["opts"]["envvar"] as $var) 505 | { 506 | $pos = strpos($var, "="); 507 | if ($pos !== false) 508 | { 509 | $key = substr($var, 0, $pos); 510 | $val = (string)substr($var, $pos + 1); 511 | 512 | foreach ($env as $key2 => $val2) 513 | { 514 | if (!strcasecmp($key, $key2)) $key = $key2; 515 | 516 | $val = str_ireplace("%" . $key2 . "%", $val2, $val); 517 | } 518 | 519 | $env[$key] = $val; 520 | } 521 | } 522 | 523 | // Set effective user and group. 524 | if (function_exists("posix_geteuid")) 525 | { 526 | $prevuid = posix_geteuid(); 527 | $prevgid = posix_getegid(); 528 | 529 | if (isset($info["args"]["opts"]["user"])) 530 | { 531 | $userinfo = $this->GetUserInfoByName($info["args"]["opts"]["user"]); 532 | if ($userinfo !== false) 533 | { 534 | posix_seteuid($userinfo["uid"]); 535 | posix_setegid($userinfo["gid"]); 536 | } 537 | } 538 | 539 | if (isset($info["args"]["opts"]["group"])) 540 | { 541 | $groupinfo = $this->GetGroupInfoByName($info["args"]["opts"]["group"]); 542 | if ($groupinfo !== false) posix_setegid($groupinfo["gid"]); 543 | } 544 | } 545 | 546 | // Windows requires redirecting pipes through sockets so they can be configured to be non-blocking. 547 | $os = php_uname("s"); 548 | 549 | if (strtoupper(substr($os, 0, 3)) == "WIN") 550 | { 551 | $serverfp = stream_socket_server("tcp://127.0.0.1:0", $errornum, $errorstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); 552 | if ($serverfp === false) 553 | { 554 | // The TCP/IP server failed to start. 555 | $this->ProcessStartFailed($info, "Localhost TCP/IP server failed to start.", $db, $uid, $id); 556 | 557 | continue; 558 | } 559 | 560 | $serverinfo = stream_socket_get_name($serverfp, false); 561 | $pos = strrpos($serverinfo, ":"); 562 | $serverip = substr($serverinfo, 0, $pos); 563 | $serverport = (int)substr($serverinfo, $pos + 1); 564 | 565 | $extraparams = array( 566 | escapeshellarg(str_replace("/", "\\", $rootpath . "/support/createprocess.exe")), 567 | "/w", 568 | "/socketip=127.0.0.1", 569 | "/socketport=" . $serverport, 570 | "/stdin=socket", 571 | "/stdout=socket", 572 | "/stderr=socket" 573 | ); 574 | 575 | $info["args"]["params"] = array_merge($extraparams, $info["args"]["params"]); 576 | } 577 | 578 | $cmd = implode(" ", $info["args"]["params"]); 579 | //echo $cmd . "\n"; 580 | 581 | // Start the process. 582 | $procpipes = array(array("pipe", "r"), array("pipe", "w"), array("pipe", "w")); 583 | $proc = @proc_open($cmd, $procpipes, $pipes, (isset($info["args"]["opts"]["dir"]) ? $info["args"]["opts"]["dir"] : NULL), $env, array("suppress_errors" => true, "bypass_shell" => true)); 584 | 585 | // Restore effective user and group. 586 | if (function_exists("posix_geteuid")) 587 | { 588 | posix_seteuid($prevuid); 589 | posix_setegid($prevgid); 590 | } 591 | 592 | if (!is_resource($proc)) 593 | { 594 | // The process/shell failed to start. 595 | $this->ProcessStartFailed($info, "Process failed to start.", $db, $uid, $id); 596 | 597 | // Remove TCP/IP server on Windows. 598 | if (strtoupper(substr($os, 0, 3)) == "WIN") fclose($serverfp); 599 | } 600 | else 601 | { 602 | // Rebuild the pipes on Windows by waiting for three valid inbound TCP/IP connections. 603 | if (strtoupper(substr($os, 0, 3)) == "WIN") 604 | { 605 | // Close the pipes created by PHP. 606 | foreach ($pipes as $fp) fclose($fp); 607 | 608 | $pipes = array(); 609 | while (count($pipes) < 3) 610 | { 611 | $readfps = array($serverfp); 612 | $writefps = array(); 613 | $exceptfps = NULL; 614 | $result = @stream_select($readfps, $writefps, $exceptfps, 1); 615 | if ($result === false) break; 616 | 617 | $info2 = @proc_get_status($proc); 618 | if (!$info2["running"]) break; 619 | 620 | if (count($readfps) && ($fp = @stream_socket_accept($serverfp)) !== false) 621 | { 622 | // Read in one byte. 623 | $num = ord(fread($fp, 1)); 624 | 625 | if ($num >= 0 && $num <= 2) $pipes[$num] = $fp; 626 | } 627 | } 628 | 629 | fclose($serverfp); 630 | 631 | if (count($pipes) < 3) 632 | { 633 | // The process/shell failed to start. 634 | $this->ProcessStartFailed($info, "The process started but failed to connect to the localhost TCP/IP server before terminating.", $db, $uid, $id); 635 | 636 | continue; 637 | } 638 | } 639 | 640 | // Move the process to the active running state. 641 | $info["proc"] = $proc; 642 | foreach ($pipes as $fp) stream_set_blocking($fp, 0); 643 | $info["pipes"] = $pipes; 644 | 645 | if (!isset($this->running[$uid])) $this->running[$uid] = array(); 646 | if (!isset($this->running[$uid][$name])) $this->running[$uid][$name] = array(); 647 | $this->running[$uid][$name][$id] = $info; 648 | 649 | @file_put_contents($info["basedir"] . "/status/" . $id . ".json", json_encode($this->GetRunningStatusResult($id, $name, $info), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 650 | 651 | $this->idmap[$uid][$id]["running"] = true; 652 | 653 | if ($db !== false) 654 | { 655 | try 656 | { 657 | $db->Query("UPDATE", array("log", array( 658 | "started" => microtime(true) 659 | ), "WHERE" => "id = ?"), $id); 660 | } 661 | catch (Exception $e) 662 | { 663 | CSS_DisplayError("A database query failed while updating the process log.", false, false); 664 | } 665 | 666 | // Notify monitors that a process started successfully. 667 | // If a monitor cares about task progress, then it can make status API calls. 668 | if ($this->HasMonitor($uid, $name)) 669 | { 670 | $result = $this->GetRunningStatusResult($id, $name, $info); 671 | 672 | $this->NotifyMonitors($uid, $name, $result); 673 | $this->NotifyMonitors($uid, "", $result); 674 | } 675 | } 676 | } 677 | } 678 | } 679 | } 680 | } 681 | } 682 | } 683 | 684 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 685 | { 686 | $this->UpdateRunningScripts(); 687 | 688 | foreach ($this->running as $uid => $names) 689 | { 690 | foreach ($names as $name => $idsinfo) 691 | { 692 | foreach ($idsinfo as $id => $info) 693 | { 694 | if (isset($info["pipes"][0])) $writefps[$prefix . "ext_scripts_" . $uid . "_" . $id . "_i"] = $info["pipes"][0]; 695 | if (isset($info["pipes"][1])) $readfps[$prefix . "ext_scripts_" . $uid . "_" . $id . "_o"] = $info["pipes"][1]; 696 | if (isset($info["pipes"][2])) $readfps[$prefix . "ext_scripts_" . $uid . "_" . $id . "_e"] = $info["pipes"][2]; 697 | } 698 | } 699 | } 700 | } 701 | 702 | public function HTTPPreProcessAPI($pathparts, $client, $userrow, $guestrow) 703 | { 704 | } 705 | 706 | public static function InitUserScriptsBasePath($userrow) 707 | { 708 | $basedir = $userrow->basepath . "/" . $userrow->id . "/scripts"; 709 | @mkdir($basedir, 0770, true); 710 | 711 | return $basedir; 712 | } 713 | 714 | public static function GetUserScriptsDB($basedir) 715 | { 716 | $filename = $basedir . "/main.db"; 717 | 718 | // Only ProcessAPI() should create the database. 719 | if (!file_exists($filename)) return array("success" => false, "error" => "The database '" . $filename . "' does not exist.", "errorcode" => "db_not_found"); 720 | 721 | $db = new CSDB_sqlite(); 722 | 723 | try 724 | { 725 | $db->Connect("sqlite:" . $filename); 726 | } 727 | catch (Exception $e) 728 | { 729 | return array("success" => false, "error" => "The database failed to open.", "errorcode" => "db_open_error"); 730 | } 731 | 732 | return array("success" => true, "db" => $db); 733 | } 734 | 735 | public function ProcessAPI($reqmethod, $pathparts, $client, $userrow, $guestrow, $data) 736 | { 737 | global $rootpath, $userhelper; 738 | 739 | $basedir = self::InitUserScriptsBasePath($userrow); 740 | 741 | $filename = $basedir . "/main.db"; 742 | 743 | $runinit = !file_exists($filename); 744 | 745 | $db = new CSDB_sqlite(); 746 | 747 | try 748 | { 749 | $db->Connect("sqlite:" . $filename); 750 | } 751 | catch (Exception $e) 752 | { 753 | return array("success" => false, "error" => "The database failed to open.", "errorcode" => "db_open_error"); 754 | } 755 | 756 | if ($runinit) 757 | { 758 | // Create database tables. 759 | if (!$db->TableExists("log")) 760 | { 761 | try 762 | { 763 | $db->Query("CREATE TABLE", array("log", array( 764 | "id" => array("INTEGER", 8, "UNSIGNED" => true, "NOT NULL" => true, "PRIMARY KEY" => true, "AUTO INCREMENT" => true), 765 | "script" => array("STRING", 1, 255, "NOT NULL" => true), 766 | "run_user" => array("STRING", 1, 255, "NOT NULL" => true), 767 | "run_group" => array("STRING", 1, 255, "NOT NULL" => true), 768 | "started" => array("FLOAT", "NOT NULL" => true), 769 | "duration" => array("FLOAT", "NOT NULL" => true), 770 | "finished" => array("FLOAT", "NOT NULL" => true), 771 | "info" => array("STRING", 4, "NOT NULL" => true), 772 | ), 773 | array( 774 | array("KEY", array("script", "duration"), "NAME" => "script_duration"), 775 | ))); 776 | } 777 | catch (Exception $e) 778 | { 779 | $db->Disconnect(); 780 | @unlink($filename); 781 | 782 | return array("success" => false, "error" => "Database table creation failed.", "errorcode" => "db_table_error"); 783 | } 784 | } 785 | 786 | // Copy staging file into directory. 787 | if (!file_exists($basedir . "/exectab.txt")) 788 | { 789 | $bytesdiff = 0; 790 | if (!file_exists($rootpath . "/user_init/scripts/exectab.txt")) $data2 = ""; 791 | else $data2 = file_get_contents($rootpath . "/user_init/scripts/exectab.txt"); 792 | $bytesdiff = strlen($data2); 793 | file_put_contents($basedir . "/exectab.txt", $data2); 794 | 795 | // Adjust total bytes stored. 796 | $userhelper->AdjustUserTotalBytes($userrow->id, $bytesdiff); 797 | } 798 | 799 | // Create status tracking directory for direct non-API access. 800 | @mkdir($basedir . "/status", 0775); 801 | } 802 | 803 | // Parse 'exectab.txt' if it has changed since the last API call. 804 | $filename = $basedir . "/exectab.txt"; 805 | if (!isset($this->exectabsts[$userrow->id])) $this->exectabsts[$userrow->id] = 0; 806 | if ($this->exectabsts[$userrow->id] < filemtime($filename) && filemtime($filename) < time()) 807 | { 808 | require_once $rootpath . "/support/cli.php"; 809 | 810 | $cmdopts = array( 811 | "shortmap" => array( 812 | "d" => "dir", 813 | "e" => "envvar", 814 | "g" => "group", 815 | "i" => "stdinallowed", 816 | "m" => "maxqueue", 817 | "n" => "noexec", 818 | "r" => "removelog", 819 | "s" => "simultaneous", 820 | "u" => "user" 821 | ), 822 | "rules" => array( 823 | "dir" => array("arg" => true), 824 | "envvar" => array("multiple" => true, "arg" => true), 825 | "group" => array("arg" => true), 826 | "stdinallowed" => array("arg" => false), 827 | "maxqueue" => array("arg" => true), 828 | "noexec" => array("arg" => false), 829 | "removelog" => array("arg" => false), 830 | "simultaneous" => array("arg" => true), 831 | "user" => array("arg" => true) 832 | ), 833 | "allow_opts_after_param" => false 834 | ); 835 | 836 | $this->exectabs[$userrow->id] = array(); 837 | $fp = fopen($filename, "rb"); 838 | while (($line = fgets($fp)) !== false) 839 | { 840 | $line = trim($line); 841 | 842 | if ($line !== "" && $line[0] !== "#" && substr($line, 0, 2) !== "//") 843 | { 844 | $args = CLI::ParseCommandLine($cmdopts, ". " . $line); 845 | 846 | if (!isset($args["opts"]["noexec"])) $args["opts"]["noexec"] = false; 847 | if (!isset($args["opts"]["removelog"])) $args["opts"]["removelog"] = false; 848 | if (!isset($args["opts"]["simultaneous"]) || $args["opts"]["simultaneous"] < 1) $args["opts"]["simultaneous"] = 1; 849 | if (!isset($args["opts"]["envvar"])) $args["opts"]["envvar"] = array(); 850 | 851 | if (count($args["params"])) 852 | { 853 | $name = array_shift($args["params"]); 854 | 855 | $this->exectabs[$userrow->id][$name] = $args; 856 | } 857 | } 858 | } 859 | fclose($fp); 860 | 861 | $this->exectabsts[$userrow->id] = filemtime($filename); 862 | 863 | // Cleanup status directory. 864 | $dir = @opendir($basedir . "/status"); 865 | if ($dir) 866 | { 867 | while (($file = readdir($dir)) !== false) 868 | { 869 | if (substr($file, -5) === ".json") 870 | { 871 | if (!isset($this->idmap[$userrow->id]) || !isset($this->idmap[$userrow->id][substr($file, 0, -5)])) @unlink($basedir . "/status/" . $file); 872 | } 873 | } 874 | 875 | closedir($dir); 876 | } 877 | } 878 | 879 | // Main API. 880 | $y = count($pathparts); 881 | if ($y < 4) return array("success" => false, "error" => "Invalid API call.", "errorcode" => "invalid_api_call"); 882 | 883 | if ($pathparts[3] === "run") 884 | { 885 | // /scripts/v1/run 886 | if ($reqmethod !== "POST") return array("success" => false, "error" => "POST request required for: /scripts/v1/run", "errorcode" => "use_post_request"); 887 | if (!isset($data["name"])) return array("success" => false, "error" => "Missing 'name'.", "errorcode" => "missing_name"); 888 | if (!isset($this->exectabs[$userrow->id][$data["name"]])) return array("success" => false, "error" => "No script found for the given name.", "errorcode" => "invalid_name"); 889 | if (!isset($data["args"])) $data["args"] = array(); 890 | if (!is_array($data["args"])) return array("success" => false, "error" => "Invalid 'args'. Expected an array.", "errorcode" => "invalid_args"); 891 | if (!isset($data["stdin"])) $data["stdin"] = ""; 892 | if (!is_string($data["stdin"])) return array("success" => false, "error" => "Invalid 'stdin'. Expected a string.", "errorcode" => "invalid_stdin"); 893 | if (!isset($data["queue"])) $data["queue"] = time(); 894 | if (!is_int($data["queue"])) return array("success" => false, "error" => "Invalid 'queue'. Expected a UNIX timestamp integer.", "errorcode" => "invalid_queue"); 895 | if ($guestrow !== false && !$guestrow->serverexts["scripts"]["run"]) return array("success" => false, "error" => "Execute/Run access denied.", "errorcode" => "access_denied"); 896 | if ($guestrow !== false && $guestrow->serverexts["scripts"]["name"] !== $data["name"]) return array("success" => false, "error" => "Script run access denied to the specified name.", "errorcode" => "access_denied"); 897 | 898 | $name = $data["name"]; 899 | $args = $this->exectabs[$userrow->id][$name]; 900 | if (!isset($this->runqueue[$userrow->id])) $this->runqueue[$userrow->id] = array(); 901 | if (!isset($this->runqueue[$userrow->id][$name])) $this->runqueue[$userrow->id][$name] = array(); 902 | 903 | if (!isset($args["opts"]["stdinallowed"]) && $data["stdin"] !== "") return array("success" => false, "error" => "The process does not allow 'stdin'. Non-empty 'stdin' string encountered.", "errorcode" => "stdin_not_allowed"); 904 | 905 | if (isset($args["opts"]["maxqueue"]) && $args["opts"]["maxqueue"] > 0) 906 | { 907 | $total = count($this->runqueue[$userrow->id][$name]); 908 | if (isset($this->running[$userrow->id][$name])) $total += count($this->running[$userrow->id][$name]); 909 | 910 | if ($total >= $args["opts"]["maxqueue"]) return array("success" => false, "error" => "The queue is full. Try again later.", "errorcode" => "queue_full"); 911 | } 912 | 913 | // Merge arguments into parameters. 914 | foreach ($args["params"] as $num => $param) 915 | { 916 | $modified = false; 917 | while (preg_match("/@@(\d+)/", $param, $match)) 918 | { 919 | $num2 = (int)$match[1]; 920 | if (!isset($data["args"][$num2 - 1])) return array("success" => false, "error" => "Missing an entry in 'args'. Expected at least " . $num2 . " entries.", "errorcode" => "invalid_args"); 921 | 922 | $param = str_replace("@@" . $num2, $data["args"][$num2 - 1], $param); 923 | 924 | $modified = true; 925 | } 926 | 927 | if ($modified && !$args["opts"]["noexec"]) $args["params"][$num] = escapeshellarg($param); 928 | } 929 | 930 | $info = array( 931 | "args" => $args, 932 | "first" => false, 933 | "last" => false, 934 | "tasks" => array() 935 | ); 936 | 937 | try 938 | { 939 | $db->Query("INSERT", array("log", array( 940 | "script" => $name, 941 | "run_user" => (isset($args["opts"]["user"]) && $this->GetUserInfoByName($args["opts"]["user"]) !== false ? $args["opts"]["user"] : (function_exists("posix_geteuid") ? $this->GetUserName(posix_geteuid()) : "")), 942 | "run_group" => (isset($args["opts"]["group"]) && $this->GetGroupInfoByName($args["opts"]["group"]) !== false ? $args["opts"]["group"] : (function_exists("posix_getegid") ? $this->GetGroupName(posix_getegid()) : "")), 943 | "started" => 0, 944 | "duration" => 0, 945 | "finished" => 0, 946 | "info" => json_encode($info, JSON_UNESCAPED_SLASHES) 947 | ), "AUTO INCREMENT" => "id")); 948 | 949 | $id = $db->GetInsertID(); 950 | } 951 | catch (Exception $e) 952 | { 953 | return array("success" => false, "error" => "A database query failed while logging the task.", "errorcode" => "db_query_error"); 954 | } 955 | 956 | // Add the process to the run queue. Processes are started/updated during core cycles. 957 | if ($data["queue"] < time()) $data["queue"] = time(); 958 | $queue = array(); 959 | $qinfo = array( 960 | "id" => $id, 961 | "queued" => $data["queue"], 962 | "queuepos" => 0, 963 | "basedir" => $basedir, 964 | "args" => $args, 965 | "proc" => false, 966 | "pipes" => false, 967 | "stdin" => $data["stdin"], 968 | "task" => false, 969 | "tasknum" => 0, 970 | "maxtasks" => 1, 971 | "taskstart" => 0, 972 | "subtask" => false, 973 | "subtaskpercent" => 0, 974 | "loginfo" => $info 975 | ); 976 | foreach ($this->runqueue[$userrow->id][$name] as $id2 => $info2) 977 | { 978 | if (!isset($queue[$id]) && $data["queue"] < $info2["queued"]) 979 | { 980 | $qinfo["queuepos"] = count($queue); 981 | $queue[$id] = $qinfo; 982 | } 983 | 984 | $info2["queuepos"] = count($queue); 985 | $queue[$id2] = $info2; 986 | 987 | if (isset($queue[$id])) 988 | { 989 | $result = $this->GetQueuedStatusResult($id2, $name, $info2); 990 | 991 | @file_put_contents($basedir . "/status/" . $id2 . ".json", json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 992 | } 993 | } 994 | if (!isset($queue[$id])) 995 | { 996 | $qinfo["queuepos"] = count($queue); 997 | $queue[$id] = $qinfo; 998 | } 999 | $this->runqueue[$userrow->id][$name] = $queue; 1000 | 1001 | if (!isset($this->idmap[$userrow->id])) $this->idmap[$userrow->id] = array(); 1002 | 1003 | $this->idmap[$userrow->id][$id] = array( 1004 | "running" => false, 1005 | "name" => $name, 1006 | "id" => $id 1007 | ); 1008 | 1009 | $info = $this->runqueue[$userrow->id][$name][$id]; 1010 | 1011 | $result = $this->GetQueuedStatusResult($id, $name, $info); 1012 | 1013 | @file_put_contents($basedir . "/status/" . $id . ".json", json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 1014 | 1015 | return $result; 1016 | } 1017 | else if ($pathparts[3] === "cancel") 1018 | { 1019 | // /scripts/v1/cancel/ID 1020 | if ($reqmethod !== "POST") return array("success" => false, "error" => "POST request required for: /scripts/v1/cancel/ID", "errorcode" => "use_post_request"); 1021 | if ($y < 5) return array("success" => false, "error" => "Missing script log ID for: /scripts/v1/cancel/ID", "errorcode" => "missing_id"); 1022 | if ($guestrow !== false && !$guestrow->serverexts["scripts"]["cancel"]) return array("success" => false, "error" => "Script cancel access denied.", "errorcode" => "access_denied"); 1023 | 1024 | $id = $pathparts[4]; 1025 | 1026 | // If the script is queued or running, then don't access the database. 1027 | if (!isset($this->idmap[$userrow->id]) || !isset($this->idmap[$userrow->id][$id])) return array("success" => false, "error" => "Script not queued or running.", "errorcode" => "script_not_queued_running"); 1028 | 1029 | $info = $this->idmap[$userrow->id][$id]; 1030 | $name = $info["name"]; 1031 | if ($guestrow !== false && $guestrow->serverexts["scripts"]["name"] !== $name) return array("success" => false, "error" => "Script status access denied to the specified name.", "errorcode" => "access_denied"); 1032 | if ($info["running"]) return array("success" => false, "error" => "Script is currently running. This API can only cancel queued scripts.", "errorcode" => "script_running"); 1033 | 1034 | $info = $this->runqueue[$userrow->id][$name][$id]; 1035 | 1036 | // Remove this entry from the run queue. 1037 | $this->RemoveRunQueueEntry($userrow->id, $name, $id); 1038 | 1039 | $info["loginfo"]["first"] = "[CANCEL]"; 1040 | $info["loginfo"]["last"] = "[CANCEL]"; 1041 | 1042 | try 1043 | { 1044 | if ($info["loginfo"]["args"]["removelog"]) 1045 | { 1046 | $db->Query("DELETE", array("log", "WHERE" => "id = ?"), array($id)); 1047 | } 1048 | else 1049 | { 1050 | $ts = microtime(true); 1051 | 1052 | $db->Query("UPDATE", array("log", array( 1053 | "started" => $ts, 1054 | "finished" => $ts, 1055 | "info" => json_encode($info["loginfo"], JSON_UNESCAPED_SLASHES) 1056 | ), "WHERE" => "id = ?"), $id); 1057 | } 1058 | } 1059 | catch (Exception $e) 1060 | { 1061 | return array("success" => false, "error" => "A database query failed while updating the process log.", "errorcode" => "db_query_error"); 1062 | } 1063 | 1064 | // Finalize the process but don't trigger any monitor notifications. 1065 | $this->FinalizeProcess($userrow->id, $name, $id, false, $info); 1066 | 1067 | return array("success" => true); 1068 | } 1069 | else if ($pathparts[3] === "status") 1070 | { 1071 | // /scripts/v1/status/ID 1072 | if ($reqmethod !== "GET") return array("success" => false, "error" => "GET request required for: /scripts/v1/status/ID", "errorcode" => "use_get_request"); 1073 | if ($guestrow !== false && !$guestrow->serverexts["scripts"]["status"]) return array("success" => false, "error" => "Script status access denied.", "errorcode" => "access_denied"); 1074 | 1075 | if ($y < 5) 1076 | { 1077 | $queued = array(); 1078 | foreach ($this->runqueue[$userrow->id] as $name => $idsinfo) $queued[$name] = ($guestrow === false || $guestrow->serverexts["scripts"]["name"] === $name ? array_keys($idsinfo) : count($idsinfo)); 1079 | 1080 | $running = array(); 1081 | foreach ($this->running[$userrow->id] as $name => $idsinfo) $running[$name] = ($guestrow === false || $guestrow->serverexts["scripts"]["name"] === $name ? array_keys($idsinfo) : count($idsinfo)); 1082 | 1083 | return array("success" => true, "queued" => (object)$queued, "running" => (object)$running); 1084 | } 1085 | else 1086 | { 1087 | $id = $pathparts[4]; 1088 | 1089 | // If the script is queued or running, then don't access the database. 1090 | if (isset($this->idmap[$userrow->id]) && isset($this->idmap[$userrow->id][$id])) 1091 | { 1092 | $info = $this->idmap[$userrow->id][$id]; 1093 | $name = $info["name"]; 1094 | if ($guestrow !== false && $guestrow->serverexts["scripts"]["name"] !== $name) return array("success" => false, "error" => "Script status access denied to the specified name.", "errorcode" => "access_denied"); 1095 | 1096 | if (!$info["running"]) 1097 | { 1098 | $info = $this->runqueue[$userrow->id][$name][$id]; 1099 | 1100 | return $this->GetQueuedStatusResult($id, $name, $info); 1101 | } 1102 | else 1103 | { 1104 | $info = $this->running[$userrow->id][$name][$id]; 1105 | 1106 | return $this->GetRunningStatusResult($id, $name, $info); 1107 | } 1108 | } 1109 | 1110 | try 1111 | { 1112 | $row = $db->GetRow("SELECT", array( 1113 | "*", 1114 | "FROM" => "?", 1115 | "WHERE" => "id = ?", 1116 | ), "log", $id); 1117 | 1118 | if (!$row) return array("success" => false, "error" => "Unable to locate a log entry with the specified ID.", "errorcode" => "invalid_id"); 1119 | 1120 | if ($guestrow !== false && $guestrow->serverexts["scripts"]["name"] !== $row->script) return array("success" => false, "error" => "Script status access denied to the specified name.", "errorcode" => "access_denied"); 1121 | } 1122 | catch (Exception $e) 1123 | { 1124 | return array("success" => false, "error" => "A database query failed while retrieving an ID.", "errorcode" => "db_query_error"); 1125 | } 1126 | 1127 | return $this->GetFinalStatusResult($row); 1128 | } 1129 | } 1130 | else if ($pathparts[3] === "monitor") 1131 | { 1132 | if ($client instanceof WebServer_Client) return array("success" => false, "error" => "WebSocket connection is required for: /scripts/v1/monitor", "errorcode" => "use_websocket"); 1133 | if ($reqmethod !== "GET") return array("success" => false, "error" => "GET request required for: /scripts/v1/monitor", "errorcode" => "use_get_request"); 1134 | if (!isset($data["name"])) return array("success" => false, "error" => "Missing 'name'.", "errorcode" => "missing_name"); 1135 | if ($data["name"] !== "" && !isset($this->exectabs[$userrow->id][$data["name"]])) return array("success" => false, "error" => "No script found for the given name.", "errorcode" => "invalid_name"); 1136 | if ($guestrow !== false && !$guestrow->serverexts["scripts"]["monitor"]) return array("success" => false, "error" => "Monitor access denied.", "errorcode" => "access_denied"); 1137 | if ($guestrow !== false && $guestrow->serverexts["scripts"]["name"] !== $data["name"]) return array("success" => false, "error" => "Script status access denied to the specified name.", "errorcode" => "access_denied"); 1138 | 1139 | $uid = $userrow->id; 1140 | $name = $data["name"]; 1141 | 1142 | if (!isset($this->monitors[$uid])) $this->monitors[$uid] = array(); 1143 | if (!isset($this->monitors[$uid][$name])) $this->monitors[$uid][$name] = array(); 1144 | 1145 | if (isset($this->monitors[$uid][$name][$client->id])) 1146 | { 1147 | unset($this->monitors[$uid][$name][$client->id]); 1148 | if (!count($this->monitors[$uid][$name])) 1149 | { 1150 | unset($this->monitors[$uid][$name]); 1151 | if (!count($this->monitors[$uid])) unset($this->monitors[$uid]); 1152 | } 1153 | 1154 | return array("success" => true, "name" => $name, "enabled" => false); 1155 | } 1156 | else 1157 | { 1158 | $this->monitors[$uid][$name][$client->id] = $data["api_sequence"]; 1159 | 1160 | return array("success" => true, "name" => $name, "enabled" => true); 1161 | } 1162 | } 1163 | else if ($pathparts[3] === "guest") 1164 | { 1165 | // Guest API. 1166 | if ($y < 5) return array("success" => false, "error" => "Invalid API call to /scripts/v1/guest.", "errorcode" => "invalid_api_call"); 1167 | if ($guestrow !== false) return array("success" => false, "error" => "Guest API key detected. Access denied to /scripts/v1/guest.", "errorcode" => "access_denied"); 1168 | if (!$userrow->serverexts["scripts"]["guests"]) return array("success" => false, "error" => "Insufficient privileges. Access denied to /scripts/v1/guest.", "errorcode" => "access_denied"); 1169 | 1170 | if ($pathparts[4] === "list") 1171 | { 1172 | // /scripts/v1/guest/list 1173 | if ($reqmethod !== "GET") return array("success" => false, "error" => "GET request required for: /scripts/v1/guest/list", "errorcode" => "use_get_request"); 1174 | 1175 | return $userhelper->GetGuestsByServerExtension($userrow->id, "scripts"); 1176 | } 1177 | else if ($pathparts[4] === "create") 1178 | { 1179 | // /scripts/v1/guest/create 1180 | if ($reqmethod !== "POST") return array("success" => false, "error" => "POST request required for: /scripts/v1/guest/create", "errorcode" => "use_post_request"); 1181 | if (!isset($data["name"])) return array("success" => false, "error" => "Missing 'name'.", "errorcode" => "missing_name"); 1182 | if (!isset($data["run"])) return array("success" => false, "error" => "Missing 'run'.", "errorcode" => "missing_run"); 1183 | if (!isset($data["cancel"])) return array("success" => false, "error" => "Missing 'cancel'.", "errorcode" => "missing_cancel"); 1184 | if (!isset($data["status"])) return array("success" => false, "error" => "Missing 'status'.", "errorcode" => "missing_status"); 1185 | if (!isset($data["monitor"])) return array("success" => false, "error" => "Missing 'monitor'.", "errorcode" => "missing_monitor"); 1186 | if (!isset($data["expires"])) return array("success" => false, "error" => "Missing 'expires'.", "errorcode" => "missing_expires"); 1187 | 1188 | $options = array( 1189 | "name" => (string)$data["name"], 1190 | "run" => (bool)(int)$data["run"], 1191 | "cancel" => (bool)(int)$data["cancel"], 1192 | "status" => (bool)(int)$data["status"], 1193 | "monitor" => (bool)(int)$data["monitor"] 1194 | ); 1195 | 1196 | $expires = (int)$data["expires"]; 1197 | 1198 | if ($expires <= time()) return array("success" => false, "error" => "Invalid 'expires' timestamp.", "errorcode" => "invalid_expires"); 1199 | 1200 | return $userhelper->CreateGuest($userrow->id, "scripts", $options, $expires); 1201 | } 1202 | else if ($pathparts[4] === "delete") 1203 | { 1204 | // /scripts/v1/guest/delete/ID 1205 | if ($reqmethod !== "DELETE") return array("success" => false, "error" => "DELETE request required for: /scripts/v1/guest/delete/ID", "errorcode" => "use_delete_request"); 1206 | if ($y < 6) return array("success" => false, "error" => "Missing ID of guest for: /scripts/v1/guest/delete/ID", "errorcode" => "missing_id"); 1207 | 1208 | return $userhelper->DeleteGuest($pathparts[5], $userrow->id); 1209 | } 1210 | } 1211 | 1212 | return array("success" => false, "error" => "Invalid API call.", "errorcode" => "invalid_api_call"); 1213 | } 1214 | } 1215 | ?> --------------------------------------------------------------------------------