├── LICENSE ├── README.md ├── classes ├── Tibia_binary_serializer.class.php └── Tibia_client.class.php ├── libs ├── XTEA.class.php ├── hhb_.inc.php └── hhb_datatypes.inc.php ├── research ├── 7.6.tibia_client.class.php ├── Player.class.php ├── searchBinary.php ├── xtea2.php ├── xtea3.php ├── xtea_helper.cpp └── xtea_helper.php └── tests ├── lazer.php ├── loginPlayerBot.php └── xtea_tests.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stefan André Brannfjell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # POTCP 2 | (P)HP (O)pen (T)ibia (C)lient (P)rotocol 3 | This is a (headless) OT client library written in PHP. 4 | 5 | It deals with the tibia protocol, rsa/xtea encryption/decryption and communication with the server. 6 | Tested against [Forgottenserver 1.3](https://github.com/otland/forgottenserver). 7 | 8 | if you wonder why there are 2 classes, Tibia_client and Tibia_client_internal, 9 | Tibia_client is supposed to be easy to use and easy to understand, easy to wrap your head around 10 | while the _internal is supposed to be... the dirty complex internals :stuck_out_tongue: 11 | 12 | ## Test: 13 | 1. Modify tests/tests/loginPlayerBot.php with your servers IP, gameserver port (usually 7172), account username and password, and character name. 14 | 2. Execute tests/loginPlayerBot.php with PHP in cli. `php loginPlayerBot.php` 15 | 3. Login on another account and see the player who logged in from PHP. 16 | 4. Ask him in-game to go in any direction, etc `go right`, `go down` and see the magic happen. 17 | 18 | ### Initial commit developed by [divinity76](https://github.com/divinity76) 19 | -------------------------------------------------------------------------------- /classes/Tibia_binary_serializer.class.php: -------------------------------------------------------------------------------- 1 | size() !== 0) { 12 | $return_warnings[] = "warning, trailing bytes i don't understand at end of {$packet_type} packet (hex): " . bin2hex($this->buf); 13 | } 14 | return $return_warnings; 15 | } 16 | public function str(): string 17 | { 18 | return $this->buf; 19 | } 20 | public function str_with_size_header(): string 21 | { 22 | return (new Tibia_binary_serializer())->add_string($this->str())->str(); 23 | } 24 | // return size of buffer 25 | public function size(): int 26 | { 27 | return strlen($this->str()); 28 | } 29 | function __construct(string $initial_buffer = "") 30 | { 31 | $this->buf = $initial_buffer; 32 | } 33 | public function eraseX(int $bytes_from_start, int $bytes_from_end = 0): self 34 | { 35 | if ($bytes_from_start < 0) { 36 | throw new \InvalidArgumentException('$bytes_from_start<0'); 37 | } 38 | if ($bytes_from_end < 0) { 39 | throw new \InvalidArgumentException('$bytes_from_end<0'); 40 | } 41 | $blen = $this->size(); 42 | $total = $bytes_from_start + $bytes_from_end; 43 | if ($total > $blen) { 44 | // is UnderflowException appropriate here? 45 | throw new \UnderflowException("requested to remove {$total} byte(s) but only {$blen} byte(s) available!"); 46 | } 47 | // unfortunately `negative zero` does not exist in this language, hence the checks are required.. 48 | if ($bytes_from_start > 0 && $bytes_from_end > 0) { 49 | $this->buf = substr($this->buf, $bytes_from_start, -$bytes_from_end); 50 | } elseif ($bytes_from_start > 0) { 51 | $this->buf = substr($this->buf, $bytes_from_start); 52 | } elseif ($bytes_from_end > 0) { 53 | $this->buf = substr($this->buf, 0, -$bytes_from_end); 54 | } else { 55 | // nothing to do, both are 0. 56 | } 57 | return $this; 58 | } 59 | // 60 | public function add(string $bytes): self 61 | { 62 | $this->buf .= $bytes; 63 | return $this; 64 | } 65 | public function add_string(string $str): self 66 | { 67 | if (($len = strlen($str)) > 0xFFFF) { 68 | throw new \InvalidArgumentException("max length of a tibia string is 65535 bytes."); 69 | } 70 | $this->buf .= to_little_uint16_t($len) . $str; 71 | return $this; 72 | } 73 | public function add_position(int $x, int $y, int $z): self 74 | { 75 | //TODO: input validation/invalidArgumentException (x < 0 > 0xFFFF y < 0 > 0xFFFF z < 0 > 0xFF ) 76 | $this->buf .= to_little_uint16_t($x) . to_little_uint16_t($y) . to_uint8_t($z); 77 | return $this; 78 | } 79 | public function addU8(int $i): self 80 | { 81 | if ($i < 0 || $i > 0xFF) { 82 | throw new \InvalidArgumentException("must be between 0-255"); 83 | } 84 | $this->buf .= to_uint8_t($i); 85 | return $this; 86 | } 87 | public function addU16(int $i): self 88 | { 89 | if ($i < 0 || $i > 0xFFFF) { 90 | throw new \InvalidArgumentException("must be between 0-65535"); 91 | } 92 | $this->buf .= to_little_uint16_t($i); 93 | return $this; 94 | } 95 | public function addU32(int $i): self 96 | { 97 | if ($i < 0 || $i > 0xFFFFFFFF) { 98 | throw new \InvalidArgumentException("must be between 0-4294967295"); 99 | } 100 | $this->buf .= to_little_uint32_t($i); 101 | return $this; 102 | } 103 | // the tibia protocol never use 64 bit (nor above) integers AFAIK, so no need to support it here. 104 | // 105 | // 106 | public function getU8(bool $exception_on_missing_bytes = true): ? int 107 | { 108 | $ret = $this->peekU8($exception_on_missing_bytes); 109 | if ($ret !== null) { 110 | $this->eraseX(1); 111 | } 112 | return $ret; 113 | } 114 | public function getU16(bool $exception_on_missing_bytes = true): ? int 115 | { 116 | $ret = $this->peekU16($exception_on_missing_bytes); 117 | if ($ret !== null) { 118 | $this->eraseX(2); 119 | } 120 | return $ret; 121 | } 122 | public function getU32(bool $exception_on_missing_bytes = true): ? int 123 | { 124 | $ret = $this->peekU32($exception_on_missing_bytes); 125 | if ($ret !== null) { 126 | $this->eraseX(4); 127 | } 128 | return $ret; 129 | } 130 | public function get_string(bool $exception_on_missing_header = true, bool $exception_on_invalid_header = true): ? string 131 | { 132 | $ret = $this->peek_string($exception_on_missing_header, $exception_on_invalid_header); 133 | if ($ret !== null) { 134 | $this->eraseX(strlen($ret) + 2); // 2: string size header 135 | } 136 | return $ret; 137 | } 138 | public function get_position(bool $exception_on_missing_bytes = true): ? array 139 | { 140 | $ret = $this->peek_position($exception_on_missing_bytes); 141 | if ($ret !== null) { 142 | $this->eraseX(5); // U16 x U16 y U8 z 143 | } 144 | return $ret; 145 | } 146 | // 147 | // 148 | // TODO: until i can decide if it should return NULL or just return `as much as possible up to $number_of_bytes`, 149 | // i'll keep this function disabled for now.. 150 | // public function peek(int $number_of_bytes, bool $exception_on_missing_bytes = true): ? string 151 | // { 152 | // if ($number_of_bytes < 0) { 153 | // throw new \InvalidArgumentException(); 154 | // } 155 | // $len = strlen($this->buf); 156 | // if ($len < $number_of_bytes) { 157 | // if ($exception_on_missing_bytes) { 158 | // // is UnderflowException correct here? 159 | // throw new \UnderflowException("{$number_of_bytes} byte(s) requested, only {$len} byte(s) available"); 160 | // } else { 161 | // return null; 162 | // } 163 | // } 164 | // return substr($this->buf, 0, $number_of_bytes); 165 | // } 166 | public function peekU8(bool $exception_on_missing_bytes = true): ? int 167 | { 168 | if ($this->size() < 1) { 169 | if ($exception_on_missing_bytes) { 170 | // is UnderflowException appropriate here? 171 | throw new \UnderflowException(); 172 | } else { 173 | return null; 174 | } 175 | } 176 | return from_uint8_t(substr($this->buf, 0, 1)); 177 | } 178 | public function peekU16(bool $exception_on_missing_bytes = true): ? int 179 | { 180 | if ($this->size() < 2) { 181 | if ($exception_on_missing_bytes) { 182 | throw new \UnderflowException(); 183 | } else { 184 | return null; 185 | } 186 | } 187 | return from_little_uint16_t(substr($this->buf, 0, 2)); 188 | } 189 | public function peekU32(bool $exception_on_missing_bytes = true): ? int 190 | { 191 | if ($this->size() < 4) { 192 | if ($exception_on_missing_bytes) { 193 | throw new \UnderflowException(); 194 | } else { 195 | return null; 196 | } 197 | } 198 | return from_little_uint32_t(substr($this->buf, 0, 4)); 199 | } 200 | public function peek_string(bool $exception_on_missing_header = true, bool $exception_on_invalid_header = true): ? string 201 | { 202 | $strlen = $this->peekU16($exception_on_missing_header); 203 | if ($strlen === null) { 204 | return null; 205 | } 206 | if (($this->size() - 2) < $strlen) { 207 | if ($exception_on_invalid_header) { 208 | throw new \UnderflowException(); 209 | } else { 210 | return null; 211 | } 212 | } 213 | return substr($this->buf, 2, $strlen); 214 | } 215 | public function peek_position(bool $exception_on_missing_bytes = true): ? array 216 | { 217 | if ($this->size() < 5) { 218 | if ($exception_on_missing_bytes) { 219 | throw new \UnderflowException(); 220 | } else { 221 | return null; 222 | } 223 | } 224 | $tmp = new Tibia_binary_serializer(substr($this->buf, 0, 5)); 225 | // i WOULD do this if order-of-execution was not important: return array('x' => $tmp->getU16(),'y' => $tmp->getU16(),'z' => $tmp->getU8()); 226 | // but it is. if it fetches z first, the return values would be corrupted garbage data. 227 | $ret = array(); 228 | $ret['x'] = $tmp->getU16(); 229 | $ret['y'] = $tmp->getU16(); 230 | $ret['z'] = $tmp->getU8(); 231 | return $ret; 232 | } 233 | // 234 | } 235 | -------------------------------------------------------------------------------- /classes/Tibia_client.class.php: -------------------------------------------------------------------------------- 1 | internal = new Tibia_client_internal($host, $port, $account, $password, $charname, $debugging); 15 | $this->internal->tibia_client = $this; 16 | } 17 | function __destruct() 18 | { 19 | unset($this->internal); // trying to force it to destruct now, this would be the appropriate time. 20 | } 21 | /** 22 | * ping the server 23 | * important to do this periodically, because if you don't, the server 24 | * will consider the connection broken, and kick you! 25 | * 26 | * @return void 27 | */ 28 | public function ping(): void 29 | { 30 | $this->internal->ping(); 31 | } 32 | const TALKTYPE_SAY = 1; 33 | const TALKTYPE_WHISPER = 2; 34 | const TALKTYPE_YELL = 3; 35 | const TALKTYPE_BROADCAST = 13; 36 | const TALKTYPE_MONSTER_SAY = 36; 37 | const TALKTYPE_MONSTER_YELL = 37; 38 | public function say(string $message, int $type = self::TALKTYPE_SAY): void 39 | { 40 | if (strlen($message) > 255) { 41 | throw new InvalidArgumentException( 42 | "message cannot be longer than 255 bytes! (PS: this is not a tibia protocol limitation, but a TFS limitation, " . 43 | "the protocol limitation is actually close to 65535 bytes.)" 44 | ); 45 | } 46 | if ($type < 0 || $type > 255) { 47 | throw new \InvalidArgumentException( 48 | "type must be between 0-255! " . 49 | "(also it can't be private-message or channel-message talk type but i cba writing the code to detect it right now)" 50 | ); 51 | } 52 | $packet = new Tibia_binary_serializer("\x96"); // 0x96: talk packet 53 | $packet->addU8($type)->add_string($message); 54 | $this->internal->send($packet->str()); 55 | } 56 | // alias of walk_north 57 | public function walk_up(int $steps = 1): void 58 | { 59 | $this->walk_north($steps); 60 | } 61 | public function walk_north(int $steps = 1): void 62 | { 63 | //todo: invalidargumentexception < 0 64 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 65 | for ($i = 0; $i < $steps; ++$i) { 66 | $this->internal->send("\x65"); 67 | } 68 | } 69 | // alias of walk_east 70 | public function walk_right(int $steps = 1): void 71 | { 72 | $this->walk_east($steps); 73 | } 74 | public function walk_east(int $steps = 1): void 75 | { 76 | //todo: invalidargumentexception < 0 77 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 78 | for ($i = 0; $i < $steps; ++$i) { 79 | $this->internal->send("\x66"); 80 | } 81 | } 82 | // alias of walk_south 83 | public function walk_down(int $steps = 1): void 84 | { 85 | $this->walk_south($steps); 86 | } 87 | public function walk_south(int $steps = 1): void 88 | { 89 | //todo: invalidargumentexception < 0 90 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 91 | for ($i = 0; $i < $steps; ++$i) { 92 | $this->internal->send("\x67"); 93 | } 94 | } 95 | // alias of walk_west 96 | public function walk_left(int $steps = 1): void 97 | { 98 | $this->walk_west($steps); 99 | } 100 | public function walk_west(int $steps = 1): void 101 | { 102 | //todo: invalidargumentexception < 0 103 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 104 | for ($i = 0; $i < $steps; ++$i) { 105 | $this->internal->send("\x68"); 106 | } 107 | } 108 | public function dance(int $moves = 10, int $msleep = 100) 109 | { 110 | // case 0x6F: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_NORTH); break; 111 | // case 0x70: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_EAST); break; 112 | // case 0x71: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_SOUTH); break; 113 | // case 0x72: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_WEST); break; 114 | $direction_bytes = "\x6F\x70\x71\x72"; 115 | $blen = strlen($direction_bytes) - 1; 116 | $last = null; 117 | for ($i = 0; $i < $moves; ++$i) { 118 | do { 119 | $neww = rand(0, $blen); 120 | } while ($neww === $last); 121 | $last = $neww; 122 | $this->internal->send($direction_bytes[$neww]); 123 | usleep($msleep * 1000); 124 | } 125 | } 126 | } 127 | class Tibia_client_internal 128 | { 129 | const TIBIA_VERSION_INT = 1097; 130 | const TIBIA_VERSION_STRING = "10.97"; 131 | // CLIENTOS_WINDOWS - it would be a major task to actually support emulating different OSs, they have different login protocols, 132 | // so for simplicity, we always say we're the Windows client. 133 | const TIBIA_CLIENT_OS_INT = 4; 134 | const TIBIA_CLIENT_OS_STRING = 'CLIENTOS_WINDOWS'; 135 | const RSA_PUBLIC_KEY = 136 | "-----BEGIN PUBLIC KEY-----\n" . 137 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbZGkDtFsHrJVlaNhzU71xZROd\n" . 138 | "15QHA7A+bdB5OZZhtKg3qmBWHXzLlFL6AIBZSQmIKrW8pYoaGzX4sQWbcrEhJhHG\n" . 139 | "FSrT27PPvuetwUKnXT11lxUJwyHFwkpb1R/UYPAbThW+sN4ZMFKKXT8VwePL9cQB\n" . 140 | "1nd+EKyqsz2+jVt/9QIDAQAB\n" . 141 | "-----END PUBLIC KEY-----\n"; // yes it is supposed to end with an \n according to openssl. 142 | /** @var Tibia_client|NULL $tibia_client */ 143 | public $tibia_client; 144 | protected $public_key_parsed_cache = null; 145 | public $debugging = false; 146 | protected $ip; 147 | protected $port; 148 | protected $account; 149 | protected $password; 150 | public $charname; 151 | protected $socket; 152 | /** @var string $xtea_key_binary */ 153 | protected $xtea_key_binary; //CS-random-generated for each instance. unless $debugging 154 | /** @var int[4] $xtea_key_intarray */ 155 | protected $xtea_key_intarray; 156 | function __construct(string $host, int $port, string $account, string $password, string $charname, bool $debugging = false) 157 | { 158 | if (strlen($account) < 1) { 159 | throw new \InvalidArgumentException("account name cannot be empty (TFS's implementation of the protocol REQUIRES a non-empty account name, even tho the tibia protocol itself technically does not.)"); 160 | } 161 | $ip = $host; 162 | if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 163 | $ip = gethostbyname($ip); 164 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) { 165 | throw new \RuntimeException("failed to get ip of hostname {$host}"); 166 | } 167 | if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 168 | throw new \RuntimeException("could only find an ipv6 address for that host, ipv6 support is (not yet?) implemented!"); 169 | } 170 | } 171 | // 172 | { 173 | $this->public_key_parsed_cache = openssl_pkey_get_public($this::RSA_PUBLIC_KEY); 174 | if (false === $this->public_key_parsed_cache) { 175 | $err = openssl_error_string(); 176 | throw new \RuntimeException("openssl_pkey_get_public() failed: {$err}"); 177 | } 178 | } 179 | $this->ip = $ip; 180 | $this->port = $port; 181 | $this->account = $account; 182 | $this->password = $password; 183 | $this->charname = $charname; 184 | $this->debugging = $debugging; 185 | $this->login(); 186 | } 187 | function __destruct() 188 | { 189 | $this->logout(); 190 | if (!!$this->public_key_parsed_cache) { 191 | openssl_pkey_free($this->public_key_parsed_cache); 192 | } 193 | } 194 | /** 195 | * little-endian adler32 196 | * (the Adler specs demands big-endian, but Cipsoft decided 197 | * "screw the rules, i have green hair" and implemented a little-endian Adler for the Tibia protocol, douchebags.) 198 | * 199 | * @param string $data 200 | * @return string binary 201 | */ 202 | public static function Adler32le(string $data): string 203 | { 204 | return strrev(hash('adler32', $data, true)); 205 | } 206 | protected function login(): void 207 | { 208 | if (!!$this->socket) { 209 | throw new \LogicException("socket already initialized during login()! "); 210 | } 211 | $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 212 | if (false === $this->socket) { 213 | $err = socket_last_error(); 214 | throw new \RuntimeException("socket_create(AF_INET, SOCK_STREAM, SOL_TCP) failed! {$err}: " . socket_strerror($err)); 215 | } 216 | if (!socket_set_block($this->socket)) { 217 | $err = socket_last_error($this->socket); 218 | throw new \RuntimeException("socket_set_block() failed! {$err}: " . socket_strerror($err)); 219 | } 220 | if (!socket_connect($this->socket, $this->ip, $this->port)) { 221 | $err = socket_last_error($this->socket); 222 | throw new \RuntimeException("socket_connect() failed! {$err}: " . socket_strerror($err)); 223 | } 224 | if (!socket_set_option($this->socket, SOL_TCP, TCP_NODELAY, 1)) { 225 | // this actually avoids some bugs, espcially if you try to talk right after login, 226 | // won't work with TCP_NODELAY disabled, but will work with TCP_NODELAY enabled. 227 | // (why? not sure.) 228 | $err = socket_last_error($this->socket); 229 | throw new \RuntimeException("setting TCP_NODELAY failed! {$err}: " . socket_strerror($err)); 230 | } 231 | // 232 | { 233 | $data = new Tibia_binary_serializer(); 234 | $data->add("\x00"); // "protocol id byte", i guess it's different for login protocol / game protocol / status protocol / etc, but seems to be ignored by TFS 235 | $data->addU16($this::TIBIA_CLIENT_OS_INT); 236 | $data->addU16($this::TIBIA_VERSION_INT); 237 | $data->add(str_repeat("\x00", 7)); // > msg.skipBytes(7); // U32 client version, U8 client type, U16 dat revision 238 | $rsa_data = new Tibia_binary_serializer(); 239 | $rsa_data->add("\x00"); // uhh... RSA decryption verification byte? (TFS considers the RSA decryption a success if this is 0 AFTER decryption.) 240 | { 241 | // 242 | if ($this->debugging) { 243 | // nice keys for debugging (but insecure) 244 | $this->xtea_key_binary = (new Tibia_binary_serializer())->add_string("xtea_key_12345")->str(); 245 | $this->xtea_key_binary = str_repeat((new Tibia_binary_serializer())->addU32(1337)->str(), 4); 246 | $this->xtea_key_binary = str_repeat("\x00", 4 * 4); 247 | } else { 248 | // secure key, not good for debugging. 249 | $this->xtea_key_binary = random_bytes(4 * 4); 250 | } 251 | assert(strlen($this->xtea_key_binary) === (4 * 4)); 252 | $this->xtea_key_intarray = XTEA::binary_key_to_int_array($this->xtea_key_binary, XTEA::PAD_NONE); 253 | assert(count($this->xtea_key_intarray) === 4); 254 | // 255 | } 256 | $rsa_data->add($this->xtea_key_binary); 257 | $rsa_data->add("\x00"); // gamemaster flag (back in tibia 7.6 it was 0 for regular players and 2 for GMs iirc. TFS ignores it.) 258 | $firstPacket = new Tibia_binary_serializer($this->read_next_packet(true, true, false, false)); 259 | if ($firstPacket->size() !== 12) { 260 | throw new \LogicException("first packet was not 12 bytes! .... " . $firstPacket->size()); 261 | } 262 | $firstPacket->eraseX(7); //TODO: what are these 7 skipped bytes? i don't know. 263 | $challengeTimestamp = $firstPacket->getU32(); 264 | $challengeRandom = $firstPacket->getU8(); 265 | assert(0 === $firstPacket->size()); 266 | $session_data = implode("\n", array( 267 | $this->account, 268 | $this->password, 269 | '???what is this token???', 270 | ((string)time()) // ??token time?? 271 | )); 272 | $rsa_data->add_string($session_data); 273 | $rsa_data->add_string($this->charname); 274 | $rsa_data->addU32($challengeTimestamp); 275 | $rsa_data->addU8($challengeRandom); 276 | $data->add($this->RSA_encrypt($rsa_data->str())); 277 | $this->send($data->str(), true, true, false); 278 | // if we don't sleep a little after logging in, nothing will work, talking, walking, etc won't respond for the first 279 | // few milliseconds or so. (???) 280 | usleep(100 * 1000); 281 | $this->ping(); // because why not.. 282 | } 283 | } 284 | 285 | /** 286 | * read next packet 287 | * if $wait_for_packet is false and no packet is available, NULL is returned. 288 | * if $remove_size_header is false, a 0-byte packet (packet only having a size header for 0 bytes) will result in an empty string. (ping packet? TCP_KEEPALIVE packet?) 289 | * if $remove_adler_checksum is true, the checksum will be removed from the returned data (after being verified - if verification fails, it's not considered an adler checksum) 290 | * if $xtea_decrypt is true, the data after the adler checksum will be xtea-decrypted. (if the length OR adler checksum is wrong, it's not considered xtea-encrypted.) 291 | * 292 | * @param boolean $wait_for_packet 293 | * @param boolean $remove_size_header 294 | * @param boolean $remove_adler_checksum 295 | * @param boolean $xtea_decrypt 296 | * @return string|null 297 | */ 298 | public function read_next_packet(bool $wait_for_packet, bool $remove_size_header = true, bool $remove_adler_checksum = true, bool $xtea_decrypt = true, bool &$adler_removed = null, bool &$xtea_decrypted = null): ?string 299 | { 300 | if ($xtea_decrypt && !$remove_adler_checksum) { 301 | throw new \InvalidArgumentException( 302 | "if \$xtea_decrypt is on, then \$remove_adler_checksum must also be on " . 303 | " (i cba writing the code required to handle that configuration right now, and the code would come with a performance penalty " 304 | . "for the common cases as well..)" 305 | ); 306 | } 307 | if ($xtea_decrypt && !$remove_size_header) { 308 | throw new \InvalidArgumentException( 309 | "if \$xtea_decrypt is on, then \$remove_size_header must be on too " . 310 | "(it's possible to fix this, but considering that the xtea scheme includes a decrypted inner_length too, " . 311 | "i believe the outer size header isn't useful anyway when xtea-decrypting...)" 312 | ); 313 | } 314 | $xtea_decrypted = false; 315 | $adler_removed = false; 316 | $flag = ($wait_for_packet ? MSG_WAITALL : MSG_DONTWAIT); 317 | $read = ''; 318 | $buf = ''; 319 | // 2 bytes: tibia packet size header, little-endian uint16 320 | $ret = socket_recv($this->socket, $buf, 2, $flag); 321 | if ($ret === 0 || ($ret === false && socket_last_error($this->socket) === SOCKET_EWOULDBLOCK)) { // 11: resource temporarily unavailable 322 | // no new packet available 323 | if (!$wait_for_packet) { 324 | // .. and we're not waiting. 325 | return null; 326 | } 327 | // FIXME socket_recv timed out even with MSG_WAITALL (it's a socksetopt option to change the timeout) 328 | return null; 329 | } 330 | if ($ret === false) { 331 | // ps: recv error at this stage probably did not corrupt the recv buffer. (unlike in the rest of this function) 332 | $erri = socket_last_error($this->socket); 333 | $err = socket_strerror($erri); 334 | throw new \RuntimeException("socket_recv error {$erri}: {$err}"); 335 | } 336 | 337 | assert(strlen($buf) >= 1); 338 | $read .= $buf; 339 | $buf = ''; 340 | if ($ret === 1) { 341 | // ... we have HALF a size header, wait for the other half regardless of $wait_for_packet (it should come ASAP anyway) 342 | // (if we don't, then the buffer is in a corrupt state where next read_next_packet will read half a size header! 343 | // - another way to handle this would be to use MSG_PEEK but oh well) 344 | $ret = socket_recv($this->socket, $buf, 1, MSG_WAITALL); 345 | if ($ret === false) { 346 | $erri = socket_last_error($this->socket); 347 | $err = socket_strerror($erri); 348 | throw new \RuntimeException("socket_recv error {$erri}: {$err} - also: the recv buffer is now in a corrupted state, " . 349 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 350 | "or a bugged server or something)"); 351 | } 352 | if ($ret !== 1) { 353 | throw new \RuntimeException("even with MSG_WAITALL we could only read half a size header! the recv buffer is now in a corrupted state, " . 354 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 355 | "or a bugged server or something)"); 356 | } 357 | assert(1 === strlen($buf)); 358 | $read .= $buf; 359 | $buf = ''; 360 | } 361 | assert(2 === strlen($read)); 362 | assert(0 === strlen($buf)); 363 | $size = from_little_uint16_t($read); 364 | 365 | while (0 < ($remaining = (($size + 2) - strlen($read)))) { 366 | $buf = ''; 367 | $ret = socket_recv($this->socket, $buf, $remaining, MSG_WAITALL); 368 | if ($ret === false) { 369 | $erri = socket_last_error($this->socket); 370 | $err = socket_strerror($erri); 371 | throw new \RuntimeException("socket_recv error {$erri}: {$err} - also: the recv buffer is now in a corrupted state, " . 372 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 373 | "or a bugged server or something)"); 374 | } 375 | if (0 === $ret) { 376 | throw new \RuntimeException("even with MSG_WAITALL and trying to read {$remaining} bytes, socket_recv return 0! something is very wrong. " . 377 | "also the recv buffer is now in a corrupted state, you should throw away this instance of TibiaClient and re-login. " . 378 | "(this should never happen btw, you probably have a very unstable connection " . 379 | "or a bugged server or something)"); 380 | } 381 | $read .= $buf; 382 | } 383 | if ($remaining !== 0) { 384 | throw new \LogicException("...wtf, after the read loop, remaining was: " . hhb_return_var_dump($remaining) . " - should never happen, probably a code bug."); 385 | } 386 | if (strlen($read) !== ($size + 2)) { 387 | throw new \LogicException('...wtf, `strlen($read) === ($size + 2)` sanity check failed, should never happen, probably a code bug.'); 388 | } 389 | assert(strlen($read) >= 2); 390 | if ($remove_size_header) { 391 | $read = substr($read, 2); 392 | } 393 | if (strlen($read) === 0) { 394 | // ping packet or TCP_KEEPALIVE packet or something i think? 395 | return ""; 396 | } 397 | if ($remove_adler_checksum && strlen($read) >= 4) { 398 | $offset = ($remove_size_header ? 0 : 2); 399 | $checksum = substr($read, $offset, 4); 400 | $checksummed_data = substr($read, $offset + 4); 401 | if (self::Adler32le($checksummed_data) === $checksum) { 402 | $adler_removed = true; 403 | $read = substr($read, 0, $offset) . $checksummed_data; 404 | } else { 405 | // unexpected, adler checksum verification failed.. 406 | } 407 | unset($offset, $checksum, $checksummed_data); 408 | } 409 | 410 | if ($xtea_decrypt && $adler_removed) { 411 | do { 412 | $offset = ($remove_size_header ? 0 : 2); 413 | $to_decrypt = substr($read, $offset); 414 | if (strlen($to_decrypt) < 8 || ((strlen($to_decrypt) % 8) !== 0)) { 415 | // this packet cannot be xtea-encrypted, wrong length. however, still weird considering the adler checksum was verified.. 416 | break; 417 | } 418 | $decrypted = XTEA::decrypt_unsafe($to_decrypt, $this->xtea_key_intarray, 32); 419 | $inner_length = from_little_uint16_t(substr($decrypted, 0, 2)); 420 | if (strlen($decrypted) < ($inner_length + 2)) { 421 | // not xtea-encrypted, wrong inner_length, however weird because the checksum was verified AND the length was correct. all conicidences? 422 | break; 423 | } 424 | $decrypted = substr($decrypted, 2, $inner_length); // 2: remove inner_length header - $inner_length: remove padding bytes (if any) 425 | $read = substr($read, 0, $offset) . $decrypted; 426 | $xtea_decrypted = true; 427 | } while (false); 428 | unset($offset, $to_decrypt, $decrypted); 429 | } 430 | return $read; 431 | } 432 | /** 433 | * ping the server 434 | * important to do this periodically, because if you don't, the server 435 | * will consider the connection broken, and kick you! 436 | * 437 | * @return void 438 | */ 439 | public function ping(): void 440 | { 441 | $this->send("\x1E"); 442 | } 443 | /** 444 | * *DEPRECATED* you should probably use Tibia_binary_serializer()->get_string() instead. 445 | * parse tibia_str 446 | * if it is a valid tibia_str, returns the tibia str, length header and trailing bytes removed. 447 | * if it's *not* a valid tibia_str, returns null 448 | * a tibia_str may be binary. 449 | * 450 | * @param string $bytes 451 | * @param integer $offset 452 | * @return string|null 453 | */ 454 | public static function parse_tibia_str(string $bytes): ?string 455 | { 456 | return (new Tibia_binary_serializer($bytes))->get_string(false, false); 457 | } 458 | const POSITION_SIZE_BYTES = 5; 459 | // *DEPRECATED* you should probably use Tibia_binary_serializer()->get_position() instead. 460 | public static function parse_position(string $bytes): ?array 461 | { 462 | return (new Tibia_binary_serializer($bytes))->get_position(false); 463 | } 464 | // *DEPRECATED* you should probably use Tibia_binary_serializer()->add_string() instead. 465 | public function tibia_str(string $str): string 466 | { 467 | return (new Tibia_binary_serializer())->add_string($str)->str(); 468 | } 469 | // openssl api has 3 different standarized padding schemes for RSA, and obviously cipsoft went all "NIH" and made their own 470 | public /*static*/ function cipsoft_rsa_pad(string &$data): void 471 | { 472 | if ((($len = strlen($data)) % 128) === 0) { 473 | return; 474 | } 475 | $nearest = (int)(ceil($len / 128) * 128); 476 | assert($nearest !== $len); 477 | assert($nearest > $len); 478 | if ($this->debugging) { 479 | $data .= str_repeat("\x00", $nearest - $len); 480 | } else { 481 | // a security-focused implementation would use CS random_bytes() instead of str_repeat. (and i think Cipsoft is doing that too.) 482 | $data .= random_bytes($nearest - $len); 483 | } 484 | return; 485 | } 486 | public function RSA_encrypt(string $data): string 487 | { 488 | assert(!!$this->public_key_parsed_cache); 489 | $crypted = ''; 490 | /// openssl padding schemes: OPENSL_PKCS1_PADDING, OPENSSL_SSLV23_PADDING, OPENSSL_PKCS1_OAEP_PADDING, OPENSSL_NO_PADDING. 491 | // openssl api has 3 different standarized padding schemes for RSA, and obviously cipsoft invented it's own incompatible one. 492 | $this->cipsoft_rsa_pad($data); 493 | assert((strlen($data) % 128) === 0); 494 | $res = openssl_public_encrypt($data, $crypted, $this->public_key_parsed_cache, OPENSSL_NO_PADDING); 495 | if (false === $res) { 496 | $err = openssl_error_string(); 497 | throw new \RuntimeException("openssl_public_encrypt() failed: {$err}"); 498 | } 499 | return $crypted; 500 | } 501 | protected function logout(): void 502 | { 503 | try { 504 | $this->send("\x14"); 505 | // TFS bug? if we send the disconnect request too fast before closing the socket, 506 | // the server will not log out the actual avatar.. 507 | //usleep(50000*1000); 508 | while ($this->read_next_packet(false, false, false, false) !== null) { 509 | //... 510 | } 511 | $this->send("\x0F"); 512 | usleep(100 * 1000); 513 | } finally { 514 | if ($this->socket) { 515 | socket_close($this->socket); 516 | } 517 | } 518 | } 519 | public function send(string $packet, bool $add_size_header = true, bool $add_adler_checksum = true, bool $xtea_encrypt = true): void 520 | { 521 | if ($xtea_encrypt) { 522 | $packet = XTEA::encrypt((new Tibia_binary_serializer())->add_string($packet)->str(), $this->xtea_key_intarray, ($this->debugging ? XTEA::PAD_0x00 : XTEA::PAD_RANDOM)); 523 | } 524 | if ($add_adler_checksum) { 525 | $packet = $this->Adler32le($packet) . $packet; 526 | } 527 | if ($add_size_header) { 528 | $len = strlen($packet); 529 | if ($len > 65535) { 530 | // note that it's still possible to have several separate packets each individually under 65535 bytes, 531 | // concantenated with the Nagle-algorithm but then you have to add the size headers and adler checksums manually, 532 | // before calling send() 533 | throw new OutOfRangeException('Cannot automatically add size header a to a packet above 65535 bytes!'); 534 | } 535 | $packet = (new Tibia_binary_serializer())->add_string($packet)->str(); 536 | } 537 | $this->socket_write_all($this->socket, $packet); 538 | } 539 | /** 540 | * writes ALL data to socket, and throws an exception if that's not possible. 541 | * 542 | * @param socket $socket 543 | * @param string $data 544 | * @return void 545 | */ 546 | public static function socket_write_all($socket, string $data): void 547 | { 548 | if (!($dlen = strlen($data))) { 549 | return; 550 | } 551 | do { 552 | assert($dlen > 0); 553 | assert(strlen($data) === $dlen); 554 | $sent_now = socket_write($socket, $data); 555 | if (false === $sent_now) { 556 | $err = socket_last_error($socket); 557 | throw new \RuntimeException("socket_write() failed! {$err}: " . socket_strerror($err)); 558 | } 559 | if (0 === $sent_now) { 560 | // we'll try *1* last time before throwing exception... 561 | $sent_now = socket_write($socket, $data); 562 | if (false === $sent_now) { 563 | $err = socket_last_error($socket); 564 | throw new \RuntimeException("socket_write() failed after first returning zero! {$err}: " . socket_strerror($err)); 565 | } 566 | if (0 === $sent_now) { 567 | // something is very wrong but it's not registering as an error at the kernel apis... 568 | throw new \RuntimeException("socket_write() keeps returning 0 bytes sent while {$dlen} byte(s) to send!"); 569 | } 570 | } 571 | $dlen -= $sent_now; 572 | $data = substr($data, $sent_now); 573 | } while ($dlen > 0); 574 | assert($dlen === 0); 575 | assert(strlen($data) === 0); 576 | // all data sent. 577 | return; 578 | } 579 | 580 | public static function parse_packet(string $packet, bool $size_header_removed = true, bool $adler_checksum_removed = true, bool $xtea_decrypted = true): Tibia_client_packet_parsed 581 | { 582 | // for now i cba writing stuff to handle size header / adler checksum / xtea encryption in here... 583 | if (!$size_header_removed) { 584 | throw new \InvalidArgumentException("remove size header before calling this function."); 585 | } 586 | if (!$adler_checksum_removed) { 587 | throw new \InvalidArgumentException("remove adler checksum before calling this function."); 588 | } 589 | if (!$xtea_decrypted) { 590 | throw new \InvalidArgumentException("decrypt xtea before calling this function."); 591 | } 592 | $ret = new Tibia_client_packet_parsed(); 593 | $ret->bytes_hex = bin2hex($packet); 594 | $len = strlen($packet); 595 | if ($len === 0) { 596 | // uhhh.... 597 | $ret->type = 0; 598 | $ret->type_name = "ping_0_bytes"; // ping_tcp_keepalive ? 599 | return $ret; 600 | } 601 | $packet = new Tibia_binary_serializer($packet); 602 | $ret->type = $packet->getU8(); 603 | switch ($ret->type) { 604 | case 0x0D: { 605 | // seems to be either ping or ping_request (eg a request that we ping back) 606 | $ret->type_name = "ping_0x0D"; 607 | return $ret; 608 | break; 609 | } 610 | case 0x17: { 611 | // TODO: better parsing of this packet, which is a big task (this packet is very very complex for some reason.) 612 | $ret->type_name = "login_and_map_and_welcome"; 613 | $packet = $packet->str(); 614 | $welcome_messages = []; 615 | $found = 0; 616 | $ret->data['welcome_messages'] = []; 617 | for ($i = strlen($packet); $i > 0; --$i) { 618 | $str = Tibia_client_internal::parse_tibia_str(substr($packet, $i)); 619 | if (null === $str) { 620 | continue; 621 | } 622 | if (strlen($str) < 1) { 623 | continue; 624 | } 625 | if (strlen($str) !== strcspn($str, "\x00\x01")) { 626 | continue; 627 | } 628 | // PROBABLY found the message. 629 | ++$found; 630 | $ret->data['welcome_messages'][] = ($str); 631 | if ($found >= 2) { 632 | break; 633 | } 634 | } 635 | return $ret; 636 | break; 637 | } 638 | case Tibia_client_packet_parsed::TYPE_SAY: // 0xAA 639 | { 640 | $ret->type_name = "TYPE_SAY"; 641 | // idk what statement_id is either.. my best guess: some weird server-global talk id used by cipsoft for debugging 642 | $ret->data["statement_id"] = $packet->getU32(); 643 | $ret->data['speaker_name'] = $packet->get_string(); 644 | $ret->data['speaker_level'] = $packet->getU16(); 645 | $ret->data['speak_type'] = $packet->getU8(); 646 | $ret->data['speaker_position'] = $packet->get_position(); 647 | $ret->data['text'] = $packet->get_string(); 648 | // Tell packet parser that your done, 649 | // if it disagrees with you, there is still data in packet. 650 | // And it will give you a warning 651 | $ret->warnings = $packet->im_done($ret->warnings, $ret->type_name); 652 | return $ret; 653 | unset($strlen); 654 | break; 655 | } 656 | default: { 657 | $ret->type_name = "unknown 0x" . bin2hex(to_uint8_t($ret->type)); 658 | return $ret; 659 | break; 660 | } 661 | } 662 | // ...unreachable? 663 | return $ret; 664 | } 665 | } 666 | 667 | class Tibia_client_packet_parsed 668 | { 669 | const TYPE_SAY = 0xAA; 670 | /** @var u8 $type */ 671 | public $type; 672 | /** @var string $type_name */ 673 | public $type_name = "unknown"; 674 | public $size_header_removed = true; 675 | public $adler_checksum_removed = true; 676 | public $xtea_decrypted = true; 677 | public $bytes_hex = ""; 678 | public $data = []; 679 | public $errors = []; 680 | public $warnings = []; 681 | } 682 | -------------------------------------------------------------------------------- /libs/XTEA.class.php: -------------------------------------------------------------------------------- 1 | 16) { 24 | throw new \InvalidArgumentException("the max length for a XTEA binary key is 16 bytes."); 25 | } elseif ($padding_scheme === self::PAD_NONE && $len !== 16) { 26 | throw new \InvalidArgumentException("with PAD_NONE the key has to be _EXACTLY_ 16 bytes long."); 27 | } elseif ($len < 16) { 28 | $key .= str_repeat("\x00", 16 - $len); 29 | } else { 30 | // all good 31 | } 32 | $ret = []; 33 | foreach (str_split($key, 4) as $key) { 34 | $ret[] = self::from_little_uint32_t($key); 35 | } 36 | assert(count($ret) === 4); 37 | return $ret; 38 | } 39 | /** 40 | * xtea-encrypt data 41 | * 42 | * @param string $data 43 | * @param int[4] $keys 44 | * @param integer $padding_scheme 45 | * @param integer $rounds 46 | * @return string 47 | */ 48 | public static function encrypt(string $data, array $keys, int $padding_scheme = self::PAD_0x00, int $rounds = 32) : string 49 | { 50 | if ($padding_scheme < 0 || $padding_scheme > 2) { 51 | throw new \InvalidArgumentException("only PAD_NONE and PAD_0x00 and PAD_RANDOM supported!"); 52 | } 53 | if (count($keys) !== 4) { 54 | throw new \InvalidArgumentException('count($keys) !== 4'); 55 | } 56 | for ($i = 0; $i < 4; ++$i) { 57 | if (!is_int($keys[$i])) { 58 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 59 | } 60 | if ($keys[$i] < 0) { 61 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 62 | } 63 | if ($keys[$i] > 0xFFFFFFFF) { 64 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 65 | } 66 | } 67 | if ($rounds < 0) { 68 | throw new \InvalidArgumentException(" < 0 rounds is impossible (and <32 is probably a bad idea)"); 69 | } 70 | $len = strlen($data); 71 | if ($len === 0 || (($len % 8) !== 0)) { 72 | if ($padding_scheme === self::PAD_NONE) { 73 | throw new \InvalidArgumentException("with PAD_NONE the data MUST be a multiple of 8 bytes!"); 74 | } else { 75 | // encrypt_unsafe will take care of it. 76 | } 77 | } 78 | // we have now verified that everything is safe. 79 | return self::encrypt_unsafe($data, $keys, $padding_scheme, $rounds); 80 | } 81 | /** 82 | * faster version of encrypt(), lacking input validation. 83 | * 84 | * @param string $data 85 | * @param int[4] $keys 86 | * @param integer $padding_scheme 87 | * @param integer $rounds 88 | * @return string 89 | */ 90 | public static function encrypt_unsafe(string $data, array $keys, int $padding_scheme = self::PAD_0x00, int $rounds = 32) : string 91 | { 92 | $len = strlen($data); 93 | if ($len === 0) { 94 | $len = 8; 95 | if ($padding_scheme === self::PAD_0x00) { 96 | $data = str_repeat("\x00", 8); 97 | } else { 98 | // self::PAD_RANDOM 99 | $data = random_bytes(8); 100 | } 101 | } elseif ((($len % 8) !== 0)) { 102 | $nearest = (int)(ceil($len / 8) * 8); 103 | assert($nearest !== $len); 104 | assert($nearest > $len); 105 | if ($padding_scheme === self::PAD_0x00) { 106 | $data .= str_repeat("\x00", $nearest - $len); 107 | } else { 108 | // self::PAD_RANDOM 109 | $data .= random_bytes($nearest - $len); 110 | } 111 | $len = $nearest; 112 | } 113 | // good to go 114 | $ret = ''; 115 | for ($i = 0; $i < $len; $i += 8) { 116 | $i1 = self::from_little_uint32_t(substr($data, $i, 4)); 117 | $i2 = self::from_little_uint32_t(substr($data, $i + 4, 4)); 118 | self::encipher_unsafe($i1, $i2, $keys, $rounds); 119 | $ret .= self::to_little_uint32_t($i1); 120 | $ret .= self::to_little_uint32_t($i2); 121 | } 122 | return $ret; 123 | } 124 | /** 125 | * xtea-decrypt data 126 | * 127 | * @param string $data 128 | * @param int[4] $keys 129 | * @param integer $rounds 130 | * @return string decrypted 131 | */ 132 | public static function decrypt(string $data, array $keys, int $rounds = 32) : string 133 | { 134 | $len = strlen($data); 135 | if ($len < 8) { 136 | throw new \InvalidArgumentException("this cannot be (intact) xtea-encrypted data, it's less than 8 bytes long (the minimum xtea length)"); 137 | } 138 | if (($len % 8) !== 0) { 139 | throw new \InvalidArgumentException("this cannot be (intact) xtea-encrypted data, the length is not a multiple of 8 bytes."); 140 | } 141 | if (count($keys) !== 4) { 142 | throw new \InvalidArgumentException('count($keys) !== 4'); 143 | } 144 | for ($i = 0; $i < 4; ++$i) { 145 | if (!is_int($keys[$i])) { 146 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 147 | } 148 | if ($keys[$i] < 0) { 149 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 150 | } 151 | if ($keys[$i] > 0xFFFFFFFF) { 152 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 153 | } 154 | } 155 | if ($rounds < 0) { 156 | throw new \InvalidArgumentException(" < 0 rounds is impossible (and <32 is probably a bad idea)"); 157 | } 158 | return self::decrypt_unsafe($data, $keys, $rounds); 159 | } 160 | /** 161 | * faster version of decrypt() but lacking input validation. 162 | * 163 | * @param string $data 164 | * @param int[4] $keys 165 | * @param integer $rounds 166 | * @return string decrypted 167 | */ 168 | public static function decrypt_unsafe(string $data, array $keys, int $rounds = 32) : string 169 | { 170 | // good to go 171 | $ret = ''; 172 | $len = strlen($data); 173 | for ($i = 0; $i < $len; $i += 8) { 174 | $i1 = self::from_little_uint32_t(substr($data, $i, 4)); 175 | $i2 = self::from_little_uint32_t(substr($data, $i + 4, 4)); 176 | self::decipher_unsafe($i1, $i2, $keys, $rounds); 177 | $ret .= self::to_little_uint32_t($i1); 178 | $ret .= self::to_little_uint32_t($i2); 179 | } 180 | return $ret; 181 | 182 | } 183 | 184 | //////////// internal functions /////////////////// 185 | protected static function from_little_uint32_t(string $i) : int 186 | { 187 | $arr = unpack('Vuint32_t', $i); 188 | return $arr['uint32_t']; 189 | } 190 | protected static function to_little_uint32_t(int $i) : string 191 | { 192 | return pack('V', $i); 193 | } 194 | protected static function encipher(int &$data1, int &$data2, array $keys, int $rounds) 195 | { 196 | { 197 | // 198 | if ($data1 < 0) { 199 | throw new \InvalidArgumentException('$data1 < 0'); 200 | } 201 | if ($data2 < 0) { 202 | throw new \InvalidArgumentException('$data2 < 0'); 203 | } 204 | if ($data1 > 0xFFFFFFFF) { 205 | throw new \InvalidArgumentException('$data1 > 0xFFFFFFFF'); 206 | } 207 | if ($data2 > 0xFFFFFFFF) { 208 | throw new \InvalidArgumentException('$data2 > 0xFFFFFFFF'); 209 | } 210 | 211 | if (count($keys) !== 4) { 212 | throw new \InvalidArgumentException('count($keys) !== 4'); 213 | } 214 | for ($i = 0; $i < 4; ++$i) { 215 | if (!is_int($keys[$i])) { 216 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 217 | } 218 | if ($keys[$i] < 0) { 219 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 220 | } 221 | if ($keys[$i] > 0xFFFFFFFF) { 222 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 223 | } 224 | } 225 | // 226 | } 227 | self::encipher_unsafe($data1, $data2, $keys, $rounds); 228 | return; // void 229 | } 230 | protected static function encipher_unsafe(int &$data1, int &$data2, array $keys, int $rounds) : void 231 | { 232 | $sum = 0; 233 | for ($i = 0; $i < $rounds; ++$i) { 234 | $data1 = self::_add( 235 | $data1, 236 | self::_add($data2 << 4 ^ self::_rshift($data2, 5), $data2) ^ 237 | self::_add($sum, $keys[$sum & 3]) 238 | ); 239 | $sum = self::_add($sum, 0x9e3779b9); // delta 240 | $data2 = self::_add( 241 | $data2, 242 | self::_add($data1 << 4 ^ self::_rshift($data1, 5), $data1) ^ 243 | self::_add($sum, $keys[self::_rshift($sum, 11) & 3]) 244 | ); 245 | } 246 | $data1 = (int)$data1; 247 | $data2 = (int)$data2; 248 | } 249 | protected static function decipher_unsafe(int &$data1, int &$data2, array $keys, int $rounds) 250 | { 251 | $sum = self::_add(0, 0x9E3779B9 * $rounds); // 0x9E3779B9 = delta 252 | for ($i = 0; $i < $rounds; ++$i) { 253 | $data2 = self::_add( 254 | $data2, 255 | -(self::_add($data1 << 4 ^ self::_rshift($data1, 5), $data1) ^ 256 | self::_add($sum, $keys[self::_rshift($sum, 11) & 3])) 257 | ); 258 | $sum = self::_add($sum, -(0x9E3779B9)); // 0x9E3779B9 = delta 259 | $data1 = self::_add( 260 | $data1, 261 | -(self::_add($data2 << 4 ^ self::_rshift($data2, 5), $data2) ^ 262 | self::_add($sum, $keys[$sum & 3])) 263 | ); 264 | } 265 | $data1 = (int)$data1; 266 | $data2 = (int)$data2; 267 | } 268 | /** 269 | * Handle proper unsigned right shift, dealing with PHP's signed shift. 270 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 271 | * @access private 272 | * @since 2004/Sep/06 273 | * @author Jeroen Derks 274 | */ 275 | protected static function _rshift($integer, $n) 276 | { 277 | // convert to 32 bits 278 | if (0xffffffff < $integer || -0xffffffff > $integer) { 279 | $integer = fmod($integer, 0xffffffff + 1); 280 | } 281 | // convert to unsigned integer 282 | if (0x7fffffff < $integer) { 283 | $integer -= 0xffffffff + 1.0; 284 | } elseif (-0x80000000 > $integer) { 285 | $integer += 0xffffffff + 1.0; 286 | } 287 | // do right shift 288 | if (0 > $integer) { 289 | $integer &= 0x7fffffff; // remove sign bit before shift 290 | $integer >>= $n; // right shift 291 | $integer |= 1 << (31 - $n); // set shifted sign bit 292 | } else { 293 | $integer >>= $n; // use normal right shift 294 | } 295 | return $integer; 296 | } 297 | 298 | /** 299 | * Handle proper unsigned add, dealing with PHP's signed add. 300 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 301 | * @access private 302 | * @since 2004/Sep/06 303 | * @author Jeroen Derks 304 | */ 305 | protected static function _add($i1, $i2) 306 | { 307 | $result = 0.0; 308 | foreach ([$i1, $i2] as $value) { 309 | // remove sign if necessary 310 | if (0.0 > $value) { 311 | $value -= 1.0 + 0xffffffff; 312 | } 313 | $result += $value; 314 | } 315 | // convert to 32 bits 316 | if (0xffffffff < $result || -0xffffffff > $result) { 317 | $result = fmod($result, 0xffffffff + 1); 318 | } 319 | // convert to signed integer 320 | if (0x7fffffff < $result) { 321 | $result -= 0xffffffff + 1.0; 322 | } elseif (-0x80000000 > $result) { 323 | $result += 0xffffffff + 1.0; 324 | } 325 | return $result; 326 | } 327 | } 328 | 329 | -------------------------------------------------------------------------------- /libs/hhb_.inc.php: -------------------------------------------------------------------------------- 1 | 22 | // version 5 ( 1372510379573 ) 23 | // v5, fixed warnings on PHP < 5.0.2 (PHP_EOL not defined), 24 | // also we can use xdebug_var_dump when available now. tested working with 5.0.0 to 5.5.0beta2 (thanks to http://viper-7.com and http://3v4l.org ) 25 | // and fixed a (corner-case) bug with "0" (empty() considders string("0") to be empty, this caused a bug in sourcecode analyze) 26 | // v4, now (tries to) tell you the source code that lead to the variables 27 | // v3, HHB_VAR_DUMP_START and HHB_VAR_DUMP_END . 28 | // v2, now compat with.. PHP5.0 + i think? tested down to 5.2.17 (previously only 5.4.0+ worked) 29 | // 30 | // 31 | $settings = array (); 32 | $PHP_EOL = "\n"; 33 | if (defined ( 'PHP_EOL' )) { // for PHP >=5.0.2 ... 34 | $PHP_EOL = PHP_EOL; 35 | } 36 | 37 | $settings ['debug_hhb_var_dump'] = false; // if true, may throw exceptions on errors.. 38 | $settings ['use_xdebug_var_dump'] = true; // try to use xdebug_var_dump (instead of var_dump) if available? 39 | $settings ['analyze_sourcecode'] = true; // false to disable the source code analyze stuff. 40 | // (it will fallback to making $settings['analyze_sourcecode']=false, if it fail to analyze the code, anyway..) 41 | $settings ['hhb_var_dump_prepend'] = 'HHB_VAR_DUMP_START' . $PHP_EOL; 42 | $settings ['hhb_var_dump_append'] = 'HHB_VAR_DUMP_END' . $PHP_EOL; 43 | // 44 | 45 | $settings ['use_xdebug_var_dump'] = ($settings ['use_xdebug_var_dump'] && is_callable ( "xdebug_var_dump" )); 46 | $argv = func_get_args (); 47 | $argc = count ( $argv, COUNT_NORMAL ); 48 | if (version_compare ( PHP_VERSION, '5.4.0', '>=' )) { 49 | $bt = debug_backtrace ( DEBUG_BACKTRACE_IGNORE_ARGS, 1 ); 50 | } else if (version_compare ( PHP_VERSION, '5.3.6', '>=' )) { 51 | $bt = debug_backtrace ( DEBUG_BACKTRACE_IGNORE_ARGS ); 52 | } else if (version_compare ( PHP_VERSION, '5.2.5', '>=' )) { 53 | $bt = debug_backtrace ( false ); 54 | } else { 55 | $bt = debug_backtrace (); 56 | } 57 | ; 58 | $analyze_sourcecode = $settings ['analyze_sourcecode']; 59 | // later, $analyze_sourcecode will be compared with $config['analyze_sourcecode'] 60 | // to determine if the reason was an error analyzing, or if it was disabled.. 61 | $bt = $bt [0]; 62 | // 63 | if ($analyze_sourcecode) { 64 | $argvSourceCode = array ( 65 | 0 => 'ignore [0]...' 66 | ); 67 | try { 68 | if (version_compare ( PHP_VERSION, '5.2.2', '<' )) { 69 | throw new Exception ( "PHP version is <5.2.2 .. see token_get_all changelog.." ); 70 | } 71 | ; 72 | $xsource = file_get_contents ( $bt ['file'] ); 73 | if (empty ( $xsource )) { 74 | throw new Exception ( 'cant get the source of ' . $bt ['file'] ); 75 | } 76 | ; 77 | $xsource .= "\n<" . '?' . 'php ignore_this_hhb_var_dump_workaround();'; // workaround, making sure that at least 1 token is an array, and has the $tok[2] >= line of hhb_var_dump 78 | $xTokenArray = token_get_all ( $xsource ); 79 | // 80 | $tmpstr = ''; 81 | $tmpUnsetKeyArray = array (); 82 | ForEach ( $xTokenArray as $xKey => $xToken ) { 83 | if (is_array ( $xToken )) { 84 | if (! array_key_exists ( 1, $xToken )) { 85 | throw new LogicException ( 'Impossible situation? $xToken is_array, but does not have $xToken[1] ...' ); 86 | } 87 | $tmpstr = trim ( $xToken [1] ); 88 | if (empty ( $tmpstr ) && $tmpstr !== '0' /*string("0") is considered "empty" -.-*/ ) { 89 | $tmpUnsetKeyArray [] = $xKey; 90 | continue; 91 | } 92 | ; 93 | switch ($xToken [0]) { 94 | case T_COMMENT : 95 | case T_DOC_COMMENT : // T_ML_COMMENT in PHP4 -.- 96 | case T_INLINE_HTML : 97 | { 98 | $tmpUnsetKeyArray [] = $xKey; 99 | continue 2; 100 | } 101 | ; 102 | default : 103 | { 104 | continue 2; 105 | } 106 | } 107 | } else if (is_string ( $xToken )) { 108 | $tmpstr = trim ( $xToken ); 109 | if (empty ( $tmpstr ) && $tmpstr !== '0' /*string("0") is considered "empty" -.-*/ ) { 110 | $tmpUnsetKeyArray [] = $xKey; 111 | } 112 | ; 113 | continue; 114 | } else { 115 | // should be unreachable.. 116 | // failed both is_array() and is_string() ??? 117 | throw new LogicException ( 'Impossible! $xToken fails both is_array() and is_string() !! .. ' ); 118 | } 119 | ; 120 | } 121 | ; 122 | ForEach ( $tmpUnsetKeyArray as $toUnset ) { 123 | unset ( $xTokenArray [$toUnset] ); 124 | } 125 | ; 126 | $xTokenArray = array_values ( $xTokenArray ); // fixing the keys.. 127 | // die(var_dump('die(var_dump(...)) in '.__FILE__.':'.__LINE__,'before:',count(token_get_all($xsource),COUNT_NORMAL),'after',count($xTokenArray,COUNT_NORMAL))); 128 | unset ( $tmpstr, $xKey, $xToken, $toUnset, $tmpUnsetKeyArray ); 129 | // 130 | $firstInterestingLineTokenKey = - 1; 131 | $lastInterestingLineTokenKey = - 1; 132 | // 133 | ForEach ( $xTokenArray as $xKey => $xToken ) { 134 | if (! is_array ( $xToken ) || ! array_key_exists ( 2, $xToken ) || ! is_integer ( $xToken [2] ) || $xToken [2] < $bt ['line']) 135 | continue; 136 | $tmpkey = $xKey; // we don't got what we want yet.. 137 | while ( true ) { 138 | if (! array_key_exists ( $tmpkey, $xTokenArray )) { 139 | throw new Exception ( '1unable to find $lastInterestingLineTokenKey !' ); 140 | } 141 | ; 142 | if ($xTokenArray [$tmpkey] === ';') { 143 | // var_dump(__LINE__.":SUCCESS WITH",$tmpkey,$xTokenArray[$tmpkey]); 144 | $lastInterestingLineTokenKey = $tmpkey; 145 | break; 146 | } 147 | // var_dump(__LINE__.":FAIL WITH ",$tmpkey,$xTokenArray[$tmpkey]); 148 | 149 | // if $xTokenArray has >=PHP_INT_MAX keys, we don't want an infinite loop, do we? ;p 150 | // i wonder how much memory that would require though.. over-engineering, err, time-wasting, ftw? 151 | if ($tmpkey >= PHP_INT_MAX) { 152 | throw new Exception ( '2unable to find $lastIntperestingLineTokenKey ! (PHP_INT_MAX reached without finding ";"...)' ); 153 | } 154 | ; 155 | ++ $tmpkey; 156 | } 157 | break; 158 | } 159 | ; 160 | if ($lastInterestingLineTokenKey <= - 1) { 161 | throw new Exception ( '3unable to find $lastInterestingLineTokenKey !' ); 162 | } 163 | ; 164 | unset ( $xKey, $xToken, $tmpkey ); 165 | // 166 | // 167 | // now work ourselves backwards from $lastInterestingLineTokenKey to the first token where $xTokenArray[$tmpi][1] == "hhb_var_dump" 168 | // i doubt this is fool-proof but.. cant think of a better way (in userland, anyway) atm.. 169 | $tmpi = $lastInterestingLineTokenKey; 170 | do { 171 | if (array_key_exists ( $tmpi, $xTokenArray ) && is_array ( $xTokenArray [$tmpi] ) && array_key_exists ( 1, $xTokenArray [$tmpi] ) && is_string ( $xTokenArray [$tmpi] [1] ) && strcasecmp ( $xTokenArray [$tmpi] [1], $bt ['function'] ) === 0) { 172 | // var_dump(__LINE__."SUCCESS WITH",$tmpi,$xTokenArray[$tmpi]); 173 | if (! array_key_exists ( $tmpi + 2, $xTokenArray )) { // +2 because [0] is (or should be) "hhb_var_dump" and [1] is (or should be) "(" 174 | throw new Exception ( '1unable to find the $firstInterestingLineTokenKey...' ); 175 | } 176 | ; 177 | $firstInterestingLineTokenKey = $tmpi + 2; 178 | break; 179 | /* */ 180 | } 181 | ; 182 | // var_dump(__LINE__."FAIL WITH ",$tmpi,$xTokenArray[$tmpi]); 183 | -- $tmpi; 184 | } while ( - 1 < $tmpi ); 185 | // die(var_dump('die(var_dump(...)) in '.__FILE__.':'.__LINE__,$tmpi)); 186 | if ($firstInterestingLineTokenKey <= - 1) { 187 | throw new Exception ( '2unable to find the $firstInterestingLineTokenKey...' ); 188 | } 189 | ; 190 | unset ( $tmpi ); 191 | // Note: $lastInterestingLineTokeyKey is likely to contain more stuff than only the stuff we want.. 192 | // 193 | // 194 | // ok, now we have $firstInterestingLineTokenKey and $lastInterestingLineTokenKey.... 195 | $interestingTokensArray = array_slice ( $xTokenArray, $firstInterestingLineTokenKey, (($lastInterestingLineTokenKey - $firstInterestingLineTokenKey) + 1) ); 196 | unset ( $addUntil, $tmpi, $tmpstr, $tmpi, $argvsourcestr, $tmpkey, $xTokenKey, $xToken ); 197 | $addUntil = array (); 198 | $tmpi = 0; 199 | $tmpstr = ""; 200 | $tmpkey = ""; 201 | $argvsourcestr = ""; 202 | // $argvSourceCode[X]='source code..'; 203 | ForEach ( $interestingTokensArray as $xTokenKey => $xToken ) { 204 | if (is_array ( $xToken )) { 205 | $tmpstr = $xToken [1]; 206 | // var_dump($xToken[1]); 207 | } else if (is_string ( $xToken )) { 208 | $tmpstr = $xToken; 209 | // var_dump($xToken); 210 | } else { 211 | /* should never reach this */ 212 | throw new LogicException ( 'Impossible situation? $xToken fails is_array() and fails is_string() ...' ); 213 | } 214 | ; 215 | $argvsourcestr .= $tmpstr; 216 | 217 | if ($xToken === '(') { 218 | $addUntil [] = ')'; 219 | continue; 220 | } else if ($xToken === '[') { 221 | $addUntil [] = ']'; 222 | continue; 223 | } 224 | ; 225 | 226 | if ($xToken === ')' || $xToken === ']') { 227 | if (false === ($tmpkey = array_search ( $xToken, $addUntil, false ))) { 228 | $argvSourceCode [] = substr ( $argvsourcestr, 0, - 1 ); // -1 is to strip the ")" 229 | if (count ( $argvSourceCode, COUNT_NORMAL ) - 1 === $argc) /*-1 because $argvSourceCode[0] is bullshit*/ { 230 | break; 231 | /* We read em all! :D (.. i hope) */ 232 | } 233 | ; 234 | /* else... oh crap */ 235 | throw new Exception ( 'failed to read source code of (what i think is) argv[' . count ( $argvSourceCode, COUNT_NORMAL ) . '] ! sorry..' ); 236 | } 237 | unset ( $addUntil [$tmpkey] ); 238 | continue; 239 | } 240 | 241 | if (empty ( $addUntil ) && $xToken === ',') { 242 | $argvSourceCode [] = substr ( $argvsourcestr, 0, - 1 ); // -1 is to strip the comma 243 | $argvsourcestr = ""; 244 | } 245 | ; 246 | } 247 | ; 248 | // die(var_dump('die(var_dump(...)) in '.__FILE__.':'.__LINE__, 249 | // $firstInterestingLineTokenKey,$lastInterestingLineTokenKey,$interestingTokensArray,$tmpstr 250 | // $argvSourceCode)); 251 | if (count ( $argvSourceCode, COUNT_NORMAL ) - 1 != $argc) /*-1 because $argvSourceCode[0] is bullshit*/ { 252 | throw new Exception ( 'failed to read source code of all the arguments! (and idk which ones i missed)! sorry..' ); 253 | } 254 | ; 255 | // 256 | } catch ( Exception $ex ) { 257 | $argvSourceCode = array (); // clear it 258 | // TODO: failed to read source code 259 | // die("TODO N STUFF..".__FILE__.__LINE__); 260 | $analyze_sourcecode = false; // ERROR.. 261 | if ($settings ['debug_hhb_var_dump']) { 262 | throw $ex; 263 | } else { 264 | /* exception ignored, continue as normal without $analyze_sourcecode */ 265 | } 266 | ; 267 | } 268 | unset ( $xsource, $xToken, $xTokenArray, $firstInterestingLineTokenKey, $lastInterestingLineTokenKey, $xTokenKey, $tmpi, $tmpkey, $argvsourcestr ); 269 | } 270 | ; 271 | // 272 | $msg = $settings ['hhb_var_dump_prepend']; 273 | if ($analyze_sourcecode != $settings ['analyze_sourcecode']) { 274 | $msg .= ' (PS: some error analyzing source code)' . $PHP_EOL; 275 | } 276 | ; 277 | $msg .= 'in "' . $bt ['file'] . '": on line "' . $bt ['line'] . '": ' . $argc . ' variable' . ($argc === 1 ? '' : 's') . $PHP_EOL; // because over-engineering ftw? 278 | if ($analyze_sourcecode) { 279 | $msg .= ' hhb_var_dump('; 280 | $msg .= implode ( ",", array_slice ( $argvSourceCode, 1 ) ); // $argvSourceCode[0] is bullshit. 281 | $msg .= ')' . $PHP_EOL; 282 | } 283 | // array_unshift($bt,$msg); 284 | echo $msg; 285 | $i = 0; 286 | foreach ( $argv as &$val ) { 287 | echo 'argv[' . ++ $i . ']'; 288 | if ($analyze_sourcecode) { 289 | echo ' >>>' . $argvSourceCode [$i] . '<<<'; 290 | } 291 | echo ':'; 292 | if ($settings ['use_xdebug_var_dump']) { 293 | xdebug_var_dump ( $val ); 294 | } else { 295 | var_dump ( $val ); 296 | } 297 | ; 298 | } 299 | 300 | echo $settings ['hhb_var_dump_append']; 301 | // call_user_func_array("var_dump",$args); 302 | } 303 | /** 304 | * works like var_dump, but returns a string instead of priting it (ob_ based) 305 | * 306 | * @param mixed $args... 307 | * @return string 308 | */ 309 | function hhb_return_var_dump(): string // works like var_dump, but returns a string instead of printing it. 310 | { 311 | $args = func_get_args (); // for <5.3.0 support ... 312 | ob_start (); 313 | call_user_func_array ( 'var_dump', $args ); 314 | return ob_get_clean (); 315 | } 316 | /** 317 | * convert a binary string to readable ascii... 318 | * 319 | * @param string $data 320 | * @param int $min_text_len 321 | * @param int $readable_min 322 | * @param int $readable_max 323 | * @return string 324 | */ 325 | function hhb_bin2readable(string $data, int $min_text_len = 3, int $readable_min = 0x40, int $readable_max = 0x7E): string { // TODO: better output.. 326 | $ret = ""; 327 | $strbuf = ""; 328 | $i = 0; 329 | for($i = 0; $i < strlen ( $data ); ++ $i) { 330 | if ($min_text_len > 0 && ord ( $data [$i] ) >= $readable_min && ord ( $data [$i] ) <= $readable_max) { 331 | $strbuf .= $data [$i]; 332 | continue; 333 | } 334 | if (strlen ( $strbuf ) >= $min_text_len && $min_text_len > 0) { 335 | $ret .= " " . $strbuf . " "; 336 | } elseif (strlen ( $strbuf ) > 0 && $min_text_len > 0) { 337 | $ret .= bin2hex ( $strbuf ); 338 | } 339 | $strbuf = ""; 340 | $ret .= bin2hex ( $data [$i] ); 341 | } 342 | if (strlen ( $strbuf ) >= $min_text_len && $min_text_len > 0) { 343 | $ret .= " " . $strbuf . " "; 344 | } elseif (strlen ( $strbuf ) > 0 && $min_text_len > 0) { 345 | $ret .= bin2hex ( $strbuf ); 346 | } 347 | $strbuf = ""; 348 | return $ret; 349 | } 350 | /** 351 | * enables hhb_exception_handler and hhb_assert_handler and sets error_reporting to max 352 | */ 353 | function hhb_init() { 354 | static $firstrun = true; 355 | if ($firstrun !== true) { 356 | return; 357 | } 358 | $firstrun = false; 359 | error_reporting ( E_ALL ); 360 | set_error_handler ( "hhb_exception_error_handler" ); 361 | // ini_set("log_errors",'On'); 362 | // ini_set("display_errors",'On'); 363 | // ini_set("log_errors_max_len",'0'); 364 | // ini_set("error_prepend_string",''); 365 | // ini_set("error_append_string",''.PHP_EOL); 366 | // ini_set("error_log",__DIR__.DIRECTORY_SEPARATOR.'error_log.php.txt'); 367 | assert_options ( ASSERT_ACTIVE, 1 ); 368 | assert_options ( ASSERT_WARNING, 0 ); 369 | assert_options ( ASSERT_QUIET_EVAL, 1 ); 370 | assert_options ( ASSERT_CALLBACK, 'hhb_assert_handler' ); 371 | } 372 | function hhb_exception_error_handler($errno, $errstr, $errfile, $errline) { 373 | if (! (error_reporting () & $errno)) { 374 | // This error code is not included in error_reporting 375 | return; 376 | } 377 | throw new ErrorException ( $errstr, 0, $errno, $errfile, $errline ); 378 | } 379 | function hhb_assert_handler($file, $line, $code, $desc = null) { 380 | $errstr = 'Assertion failed at ' . $file . ':' . $line . ' ' . $desc . ' code: ' . $code; 381 | throw new ErrorException ( $errstr, 0, 1, $file, $line ); 382 | } 383 | function hhb_combine_filepaths( /*...*/ ):string { 384 | $args = func_get_args (); 385 | if (count ( $args ) == 0) { 386 | return ""; 387 | } 388 | $ret = ""; 389 | $i = 0; 390 | foreach ( $args as $arg ) { 391 | ++ $i; 392 | if ($i != 1) { 393 | $ret .= DIRECTORY_SEPARATOR; 394 | } 395 | $ret .= str_replace ( (DIRECTORY_SEPARATOR === '/' ? '\\' : '/'), DIRECTORY_SEPARATOR, $arg ) . DIRECTORY_SEPARATOR; 396 | } 397 | while ( false !== stripos ( $ret, DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR ) ) { 398 | $ret = str_replace ( DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $ret ); 399 | } 400 | if (strlen ( $ret ) < 2) { 401 | return $ret; // edge case: a single DIRECTORY_SEPARATOR empty 402 | } 403 | if ($ret [strlen ( $ret ) - 1] === DIRECTORY_SEPARATOR) { 404 | $ret = substr ( $ret, 0, - 1 ); 405 | } 406 | return $ret; 407 | } 408 | class hhb_curl { 409 | protected $curlh; 410 | protected $curloptions = [ ]; 411 | protected $response_body_file_handle; // CURLOPT_FILE 412 | protected $response_headers_file_handle; // CURLOPT_WRITEHEADER 413 | protected $request_body_file_handle; // CURLOPT_INFILE 414 | protected $stderr_file_handle; // CURLOPT_STDERR 415 | protected function truncateFileHandles() { 416 | $trun = ftruncate ( $this->response_body_file_handle, 0 ); 417 | assert ( true === $trun ); 418 | $trun = ftruncate ( $this->response_headers_file_handle, 0 ); 419 | assert ( true === $trun ); 420 | // $trun = ftruncate ( $this->request_body_file_handle, 0 ); 421 | // assert ( true === $trun ); 422 | $trun = ftruncate ( $this->stderr_file_handle, 0 ); 423 | assert ( true === $trun ); 424 | return /*true*/; 425 | } 426 | /** 427 | * returns the internal curl handle 428 | * 429 | * its probably a bad idea to mess with it, you'll probably never want to use this function. 430 | * 431 | * @return resource_curl 432 | */ 433 | public function _getCurlHandle()/*: curlresource*/ { 434 | return $this->curlh; 435 | } 436 | /** 437 | * replace the internal curl handle with another one... 438 | * 439 | * its probably a bad idea. you'll probably never want to use this function. 440 | * 441 | * @param resource_curl $newcurl 442 | * @param bool $closeold 443 | * @throws InvalidArgumentsException 444 | * 445 | * @return void 446 | */ 447 | public function _replaceCurl($newcurl, bool $closeold = true) { 448 | if (! is_resource ( $newcurl )) { 449 | throw new InvalidArgumentsException ( 'parameter 1 must be a curl resource!' ); 450 | } 451 | if (get_resource_type ( $newcurl ) !== 'curl') { 452 | throw new InvalidArgumentsException ( 'parameter 1 must be a curl resource!' ); 453 | } 454 | if ($closeold) { 455 | curl_close ( $this->curlh ); 456 | } 457 | $this->curlh = $newcurl; 458 | $this->_prepare_curl (); 459 | } 460 | /** 461 | * mimics curl_init, using hhb_curl::__construct 462 | * 463 | * @param string $url 464 | * @param bool $insecureAndComfortableByDefault 465 | * @return hhb_curl 466 | */ 467 | public static function init(string $url = null, bool $insecureAndComfortableByDefault = false): hhb_curl { 468 | return new hhb_curl ( $url, $insecureAndComfortableByDefault ); 469 | } 470 | /** 471 | * 472 | * @param string $url 473 | * @param bool $insecureAndComfortableByDefault 474 | * @throws RuntimeException 475 | */ 476 | function __construct(string $url = null, bool $insecureAndComfortableByDefault = false) { 477 | $this->curlh = curl_init ( '' ); // why empty string? PHP Fatal error: Uncaught TypeError: curl_init() expects parameter 1 to be string, null given 478 | if (! $this->curlh) { 479 | throw new RuntimeException ( 'curl_init failed!' ); 480 | } 481 | if ($url !== null) { 482 | $this->_setopt ( CURLOPT_URL, $url ); 483 | } 484 | $fhandles = [ ]; 485 | $tmph = NULL; 486 | for($i = 0; $i < 4; ++ $i) { 487 | $tmph = tmpfile (); 488 | if ($tmph === false) { 489 | // for($ii = 0; $ii < $i; ++ $ii) { 490 | // // @fclose($fhandles[$ii]);//yay, potentially overwriting last error to fuck your debugging efforts! 491 | // } 492 | throw new RuntimeException ( 'tmpfile() failed to create 4 file handles!' ); 493 | } 494 | $fhandles [] = $tmph; 495 | } 496 | unset ( $tmph ); 497 | $this->response_body_file_handle = $fhandles [0]; // CURLOPT_FILE 498 | $this->response_headers_file_handle = $fhandles [1]; // CURLOPT_WRITEHEADER 499 | $this->request_body_file_handle = $fhandles [2]; // CURLOPT_INFILE 500 | $this->stderr_file_handle = $fhandles [3]; // CURLOPT_STDERR 501 | unset ( $fhandles ); 502 | $this->_prepare_curl (); 503 | if ($insecureAndComfortableByDefault) { 504 | $this->_setComfortableOptions (); 505 | } 506 | } 507 | function __destruct() { 508 | curl_close ( $this->curlh ); 509 | fclose ( $this->response_body_file_handle ); // CURLOPT_FILE 510 | fclose ( $this->response_headers_file_handle ); // CURLOPT_WRITEHEADER 511 | fclose ( $this->request_body_file_handle ); // CURLOPT_INFILE 512 | fclose ( $this->stderr_file_handle ); // CURLOPT_STDERR 513 | } 514 | /** 515 | * sets some insecure, but comfortable settings.. 516 | * 517 | * @return self 518 | */ 519 | public function _setComfortableOptions(): self { 520 | $this->setopt_array ( array ( 521 | CURLOPT_AUTOREFERER => true, 522 | CURLOPT_BINARYTRANSFER => true, 523 | CURLOPT_FOLLOWLOCATION => true, 524 | CURLOPT_HTTPGET => true, 525 | CURLOPT_SSL_VERIFYPEER => false, 526 | CURLOPT_CONNECTTIMEOUT => 4, 527 | CURLOPT_TIMEOUT => 8, 528 | CURLOPT_COOKIEFILE => "", // < "", // << makes curl post all supported encodings, gzip/deflate/etc, makes transfers faster 530 | CURLOPT_USERAGENT => 'hhb_curl_php; curl/' . $this->version () ['version'] . ' (' . $this->version () ['host'] . '); php/' . PHP_VERSION 531 | ) ); // 532 | return $this; 533 | } 534 | /** 535 | * curl_errno — Return the last error number 536 | * 537 | * @return int 538 | */ 539 | public function errno(): int { 540 | return curl_errno ( $this->curlh ); 541 | } 542 | /** 543 | * curl_error — Return a string containing the last error 544 | * 545 | * @return string 546 | */ 547 | public function error(): string { 548 | return curl_error ( $this->curlh ); 549 | } 550 | /** 551 | * curl_escape — URL encodes the given string 552 | * 553 | * @param string $str 554 | * @return string 555 | */ 556 | public function escape(string $str): string { 557 | return curl_escape ( $this->curlh, $str ); 558 | } 559 | /** 560 | * curl_unescape — Decodes the given URL encoded string 561 | * 562 | * @param string $str 563 | * @return string 564 | */ 565 | public function unescape(string $str): string { 566 | return curl_unescape ( $this->curlh, $str ); 567 | } 568 | /** 569 | * executes the curl request (curl_exec) 570 | * 571 | * @param string $url 572 | * @throws RuntimeException 573 | * @return self 574 | */ 575 | public function exec(string $url = null): self { 576 | $this->truncateFileHandles (); 577 | // WARNING: some weird error where curl will fill up the file again with 00's when the file has been truncated 578 | // until it is the same size as it was before truncating, then keep appending... 579 | // hopefully this _prepare_curl() call will fix that.. (seen on debian 8 on btrfs with curl/7.38.0) 580 | $this->_prepare_curl (); 581 | if (is_string ( $url ) && strlen ( $url ) > 0) { 582 | $this->setopt ( CURLOPT_URL, $url ); 583 | } 584 | $ret = curl_exec ( $this->curlh ); 585 | if ($this->errno ()) { 586 | throw new RuntimeException ( 'curl_exec failed. errno: ' . var_export ( $this->errno (), true ) . ' error: ' . var_export ( $this->error (), true ) ); 587 | } 588 | return $this; 589 | } 590 | /** 591 | * Create a CURLFile object for use with CURLOPT_POSTFIELDS 592 | * 593 | * @param string $filename 594 | * @param string $mimetype 595 | * @param string $postname 596 | * @return CURLFile 597 | */ 598 | public function file_create(string $filename, string $mimetype = null, string $postname = null): CURLFile { 599 | return curl_file_create ( $filename, $mimetype, $postname ); 600 | } 601 | /** 602 | * Get information regarding the last transfer 603 | * 604 | * @param int $opt 605 | * @return mixed 606 | */ 607 | public function getinfo(int $opt) { 608 | return curl_getinfo ( $this->curlh, $opt ); 609 | } 610 | // pause is explicitly undocumented for now, but it pauses a running transfer 611 | public function pause(int $bitmask): int { 612 | return curl_pause ( $this->curlh, $bitmask ); 613 | } 614 | /** 615 | * Reset all options 616 | */ 617 | public function reset(): self { 618 | curl_reset ( $this->curlh ); 619 | $this->curloptions = [ ]; 620 | $this->_prepare_curl (); 621 | return $this; 622 | } 623 | /** 624 | * curl_setopt_array — Set multiple options for a cURL transfer 625 | * 626 | * @param array $options 627 | * @throws InvalidArgumentException 628 | * @return self 629 | */ 630 | public function setopt_array(array $options): self { 631 | foreach ( $options as $option => $value ) { 632 | $this->setopt ( $option, $value ); 633 | } 634 | return $this; 635 | } 636 | /** 637 | * gets the last response body 638 | * 639 | * @return string 640 | */ 641 | public function getResponseBody(): string { 642 | return file_get_contents ( stream_get_meta_data ( $this->response_body_file_handle ) ['uri'] ); 643 | } 644 | /** 645 | * returns the response headers of the last request (when auto-following Location-redirect, only the last headers are returned) 646 | * 647 | * @return string[] 648 | */ 649 | public function getResponseHeaders(): array { 650 | $text = file_get_contents ( stream_get_meta_data ( $this->response_headers_file_handle ) ['uri'] ); 651 | // ... 652 | return $this->splitHeaders ( $text ); 653 | } 654 | /** 655 | * gets the response headers of all the requets for the last execution (including any Location-redirect autofollow headers) 656 | * 657 | * @return string[][] 658 | */ 659 | public function getResponsesHeaders(): array { 660 | // var_dump($this->getStdErr());die(); 661 | // CONSIDER https://bugs.php.net/bug.php?id=65348 662 | $Cr = "\x0d"; 663 | $Lf = "\x0a"; 664 | $CrLf = "\x0d\x0a"; 665 | $stderr = $this->getStdErr (); 666 | $responses = [ ]; 667 | while ( FALSE !== ($startPos = strpos ( $stderr, $Lf . '<' )) ) { 668 | $stderr = substr ( $stderr, $startPos + strlen ( $Lf ) ); 669 | $endPos = strpos ( $stderr, $CrLf . "<\x20" . $CrLf ); 670 | if ($endPos === false) { 671 | // ofc, curl has ths quirk where the specific message "* HTTP error before end of send, stop sending" gets appended with LF instead of the usual CRLF for other messages... 672 | $endPos = strpos ( $stderr, $Lf . "<\x20" . $CrLf ); 673 | } 674 | // var_dump(bin2hex(substr($stderr,279,30)),$endPos);die("HEX"); 675 | // var_dump($stderr,$endPos);die("PAIN"); 676 | assert ( $endPos !== FALSE ); // should always be more after this with CURLOPT_VERBOSE.. (connection left intact / connecton dropped /whatever) 677 | $headers = substr ( $stderr, 0, $endPos ); 678 | // $headerscpy=$headers; 679 | $stderr = substr ( $stderr, $endPos + strlen ( $CrLf . $CrLf ) ); 680 | $headers = preg_split ( "/((\r?\n)|(\r\n?))/", $headers ); // i can NOT explode($CrLf,$headers); because sometimes, in the middle of recieving headers, it will spout stuff like "\n* Added cookie reg_ext_ref="deleted" for domain facebook.com, path /, expire 1457503459" 681 | // if(strpos($headerscpy,"report-uri=")!==false){ 682 | // //var_dump($headerscpy);die("DIEDS"); 683 | // var_dump($headers); 684 | // //var_dump($this->getStdErr());die("DIEDS"); 685 | // } 686 | foreach ( $headers as $key => &$val ) { 687 | $val = trim ( $val ); 688 | if (! strlen ( $val )) { 689 | unset ( $headers [$key] ); 690 | continue; 691 | } 692 | if ($val [0] !== '<') { 693 | // static $r=0;++$r;var_dump('removing',$val);if($r>1)die(); 694 | unset ( $headers [$key] ); // sometimes, in the middle of recieving headers, it will spout stuff like "\n* Added cookie reg_ext_ref="deleted" for domain facebook.com, path /, expire 1457503459" 695 | continue; 696 | } 697 | $val = trim ( substr ( $val, 1 ) ); 698 | } 699 | unset ( $val ); // references can be scary.. 700 | $responses [] = $headers; 701 | } 702 | unset ( $headers, $key, $val, $endPos, $startPos ); 703 | return $responses; 704 | } 705 | // we COULD have a getResponsesCookies too... 706 | /* 707 | * get last response cookies 708 | * 709 | * @return string[] 710 | */ 711 | public function getResponseCookies(): array { 712 | $headers = $this->getResponsesHeaders (); 713 | $headers_merged = array (); 714 | foreach ( $headers as $headers2 ) { 715 | foreach ( $headers2 as $header ) { 716 | $headers_merged [] = $header; 717 | } 718 | } 719 | return $this->parseCookies ( $headers_merged ); 720 | } 721 | // explicitly undocumented for now.. 722 | public function getRequestBody(): string { 723 | return file_get_contents ( stream_get_meta_data ( $this->request_body_file_handle ) ['uri'] ); 724 | } 725 | /** 726 | * return headers of last execution 727 | * 728 | * @return string[] 729 | */ 730 | public function getRequestHeaders(): array { 731 | $requestsHeaders = $this->getRequestsHeaders (); 732 | $requestCount = count ( $requestsHeaders ); 733 | if ($requestCount === 0) { 734 | return array (); 735 | } 736 | return $requestsHeaders [$requestCount - 1]; 737 | } 738 | // array(0=>array(request1_headers),1=>array(requst2_headers),2=>array(request3_headers))~ 739 | /** 740 | * get last execution request headers 741 | * 742 | * @return string[] 743 | */ 744 | public function getRequestsHeaders(): array { 745 | // CONSIDER https://bugs.php.net/bug.php?id=65348 746 | $Cr = "\x0d"; 747 | $Lf = "\x0a"; 748 | $CrLf = "\x0d\x0a"; 749 | $stderr = $this->getStdErr (); 750 | $requests = [ ]; 751 | while ( FALSE !== ($startPos = strpos ( $stderr, $Lf . '>' )) ) { 752 | $stderr = substr ( $stderr, $startPos + strlen ( $Lf . '>' ) ); 753 | $endPos = strpos ( $stderr, $CrLf . $CrLf ); 754 | if ($endPos === false) { 755 | // ofc, curl has ths quirk where the specific message "* HTTP error before end of send, stop sending" gets appended with LF instead of the usual CRLF for other messages... 756 | $endPos = strpos ( $stderr, $Lf . $CrLf ); 757 | } 758 | assert ( $endPos !== FALSE ); // should always be more after this with CURLOPT_VERBOSE.. (connection left intact / connecton dropped /whatever) 759 | $headers = substr ( $stderr, 0, $endPos ); 760 | $stderr = substr ( $stderr, $endPos + strlen ( $CrLf . $CrLf ) ); 761 | $headers = explode ( $CrLf, $headers ); 762 | foreach ( $headers as $key => &$val ) { 763 | $val = trim ( $val ); 764 | if (! strlen ( $val )) { 765 | unset ( $headers [$key] ); 766 | } 767 | } 768 | unset ( $val ); // references can be scary.. 769 | $requests [] = $headers; 770 | } 771 | unset ( $headers, $key, $val, $endPos, $startPos ); 772 | return $requests; 773 | } 774 | /** 775 | * return last execution request cookies 776 | * 777 | * @return string[] 778 | */ 779 | public function getRequestCookies(): array { 780 | return $this->parseCookies ( $this->getRequestHeaders () ); 781 | } 782 | /** 783 | * get everything curl wrote to stderr of the last execution 784 | * 785 | * @return string 786 | */ 787 | public function getStdErr(): string { 788 | return file_get_contents ( stream_get_meta_data ( $this->stderr_file_handle ) ['uri'] ); 789 | } 790 | /** 791 | * alias of getResponseBody 792 | * 793 | * @return string 794 | */ 795 | public function getStdOut(): string { 796 | return $this->getResponseBody (); 797 | } 798 | protected function splitHeaders(string $headerstring): array { 799 | $headers = preg_split ( "/((\r?\n)|(\r\n?))/", $headerstring ); 800 | foreach ( $headers as $key => $val ) { 801 | if (! strlen ( trim ( $val ) )) { 802 | unset ( $headers [$key] ); 803 | } 804 | } 805 | return $headers; 806 | } 807 | protected function parseCookies(array $headers): array { 808 | $returnCookies = [ ]; 809 | $grabCookieName = function ($str, &$len) { 810 | $len = 0; 811 | $ret = ""; 812 | $i = 0; 813 | for($i = 0; $i < strlen ( $str ); ++ $i) { 814 | ++ $len; 815 | if ($str [$i] === ' ') { 816 | continue; 817 | } 818 | if ($str [$i] === '=' || $str [$i] === ';') { 819 | -- $len; 820 | break; 821 | } 822 | $ret .= $str [$i]; 823 | } 824 | return urldecode ( $ret ); 825 | }; 826 | foreach ( $headers as $header ) { 827 | // Set-Cookie: crlfcoookielol=crlf+is%0D%0A+and+newline+is+%0D%0A+and+semicolon+is%3B+and+not+sure+what+else 828 | /* 829 | * Set-Cookie:ci_spill=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22305d3d67b8016ca9661c3b032d4319df%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%2285.164.158.128%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+6.1%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F43.0.2357.132+Safari%2F537.36%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1436874639%3B%7Dcab1dd09f4eca466660e8a767856d013; expires=Tue, 14-Jul-2015 13:50:39 GMT; path=/ 830 | * Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT; 831 | * //Cookie names cannot contain any of the following '=,; \t\r\n\013\014' 832 | * // 833 | */ 834 | if (stripos ( $header, "Set-Cookie:" ) !== 0) { 835 | continue; 836 | /* */ 837 | } 838 | $header = trim ( substr ( $header, strlen ( "Set-Cookie:" ) ) ); 839 | $len = 0; 840 | while ( strlen ( $header ) > 0 ) { 841 | $cookiename = $grabCookieName ( $header, $len ); 842 | $returnCookies [$cookiename] = ''; 843 | $header = substr ( $header, $len ); 844 | if (strlen ( $header ) < 1) { 845 | break; 846 | } 847 | if ($header [0] === '=') { 848 | $header = substr ( $header, 1 ); 849 | } 850 | $thepos = strpos ( $header, ';' ); 851 | if ($thepos === false) { // last cookie in this Set-Cookie. 852 | $returnCookies [$cookiename] = urldecode ( $header ); 853 | break; 854 | } 855 | $returnCookies [$cookiename] = urldecode ( substr ( $header, 0, $thepos ) ); 856 | $header = trim ( substr ( $header, $thepos + 1 ) ); // also remove the ; 857 | } 858 | } 859 | unset ( $header, $cookiename, $thepos ); 860 | return $returnCookies; 861 | } 862 | /** 863 | * Set an option for curl 864 | * 865 | * @param int $option 866 | * @param mixed $value 867 | * @throws InvalidArgumentException 868 | * @return self 869 | */ 870 | public function setopt(int $option, $value): self { 871 | switch ($option) { 872 | case CURLOPT_VERBOSE : 873 | { 874 | trigger_error ( 'you should NOT change CURLOPT_VERBOSE. use getStdErr() instead. we are working around https://bugs.php.net/bug.php?id=65348 using CURLOPT_VERBOSE.', E_USER_WARNING ); 875 | break; 876 | } 877 | case CURLOPT_RETURNTRANSFER : 878 | { 879 | trigger_error ( 'you should NOT use CURLOPT_RETURNTRANSFER. use getResponseBody() instead. expect problems now.', E_USER_WARNING ); 880 | break; 881 | } 882 | case CURLOPT_FILE : 883 | { 884 | trigger_error ( 'you should NOT use CURLOPT_FILE. use getResponseBody() instead. expect problems now.', E_USER_WARNING ); 885 | break; 886 | } 887 | case CURLOPT_WRITEHEADER : 888 | { 889 | trigger_error ( 'you should NOT use CURLOPT_WRITEHEADER. use getResponseHeaders() instead. expect problems now.', E_USER_WARNING ); 890 | break; 891 | } 892 | case CURLOPT_INFILE : 893 | { 894 | trigger_error ( 'you should NOT use CURLOPT_INFILE. use setRequestBody() instead. expect problems now.', E_USER_WARNING ); 895 | break; 896 | } 897 | case CURLOPT_STDERR : 898 | { 899 | trigger_error ( 'you should NOT use CURLOPT_STDERR. use getStdErr() instead. expect problems now.', E_USER_WARNING ); 900 | break; 901 | } 902 | case CURLOPT_HEADER : 903 | { 904 | trigger_error ( 'you NOT use CURLOPT_HEADER. use getResponsesHeaders() instead. expect problems now. we are working around https://bugs.php.net/bug.php?id=65348 using CURLOPT_VERBOSE, which is, until the bug is fixed, is incompatible with CURLOPT_HEADER.', E_USER_WARNING ); 905 | break; 906 | } 907 | case CURLINFO_HEADER_OUT : 908 | { 909 | trigger_error ( 'you should NOT use CURLINFO_HEADER_OUT. use getRequestHeaders() instead. expect problems now.', E_USER_WARNING ); 910 | break; 911 | } 912 | 913 | default : 914 | { 915 | } 916 | } 917 | return $this->_setopt ( $option, $value ); 918 | } 919 | /** 920 | * 921 | * @param int $option 922 | * @param unknown $value 923 | * @throws InvalidArgumentException 924 | * @return self 925 | */ 926 | private function _setopt(int $option, $value): self { 927 | $ret = curl_setopt ( $this->curlh, $option, $value ); 928 | if (! $ret) { 929 | throw new InvalidArgumentException ( 'curl_setopt failed. errno: ' . $this->errno () . '. error: ' . $this->error () . '. option: ' . var_export ( $this->_curlopt_name ( $option ), true ) . ' (' . var_export ( $option, true ) . '). value: ' . var_export ( $value, true ) ); 930 | } 931 | $this->curloptions [$option] = $value; 932 | return $this; 933 | } 934 | /** 935 | * return an option previously given to setopt(_array) 936 | * 937 | * @param int $option 938 | * @param bool $isset 939 | * @return mixed|NULL 940 | */ 941 | public function getopt(int $option, bool &$isset = NULL) { 942 | if (array_key_exists ( $option, $this->curloptions )) { 943 | $isset = true; 944 | return $this->curloptions [$option]; 945 | } else { 946 | $isset = false; 947 | return NULL; 948 | } 949 | } 950 | /** 951 | * return a string representation of the given curl error code 952 | * 953 | * (ps, most of the time you'll probably want to use error() instead of strerror()) 954 | * 955 | * @param int $errornum 956 | * @return string 957 | */ 958 | public function strerror(int $errornum): string { 959 | return curl_strerror ( $errornum ); 960 | } 961 | /** 962 | * gets cURL version information 963 | * 964 | * @param int $age 965 | * @return array 966 | */ 967 | public function version(int $age = CURLVERSION_NOW): array { 968 | return curl_version ( $age ); 969 | } 970 | private function _prepare_curl() { 971 | $this->truncateFileHandles (); 972 | $this->_setopt ( CURLOPT_FILE, $this->response_body_file_handle ); // CURLOPT_FILE 973 | $this->_setopt ( CURLOPT_WRITEHEADER, $this->response_headers_file_handle ); // CURLOPT_WRITEHEADER 974 | $this->_setopt ( CURLOPT_INFILE, $this->request_body_file_handle ); // CURLOPT_INFILE 975 | $this->_setopt ( CURLOPT_STDERR, $this->stderr_file_handle ); // CURLOPT_STDERR 976 | $this->_setopt ( CURLOPT_VERBOSE, true ); 977 | } 978 | /** 979 | * gets the constants name of the given curl options 980 | * 981 | * useful for error messages (instead of "FAILED TO SET CURLOPT 21387" , you can say "FAILED TO SET CURLOPT_VERBOSE" ) 982 | * 983 | * @param int $option 984 | * @return mixed|boolean 985 | */ 986 | public function _curlopt_name(int $option)/*:mixed(string|false)*/{ 987 | // thanks to TML for the get_defined_constants trick.. 988 | // If you had some specific reason for doing it with your current approach (which is, to me, approaching the problem completely backwards - "I dug a hole! How do I get out!"), it seems that your entire function there could be replaced with: return array_flip(get_defined_constants(true)['curl']); 989 | $curldefs = array_flip ( get_defined_constants ( true ) ['curl'] ); 990 | if (isset ( $curldefs [$option] )) { 991 | return $curldefs [$option]; 992 | } else { 993 | return false; 994 | } 995 | } 996 | /** 997 | * gets the constant number of the given constant name 998 | * 999 | * (what was i thinking!?) 1000 | * 1001 | * @param string $option 1002 | * @return int|boolean 1003 | */ 1004 | public function _curlopt_number(string $option)/*:mixed(int|false)*/{ 1005 | // thanks to TML for the get_defined_constants trick.. 1006 | $curldefs = get_defined_constants ( true ) ['curl']; 1007 | if (isset ( $curldefs [$option] )) { 1008 | return $curldefs [$option]; 1009 | } else { 1010 | return false; 1011 | } 1012 | } 1013 | } 1014 | class hhb_bcmath { 1015 | public $scale = 200; 1016 | public function __construct(int $scale = 200) { 1017 | $this->scale = $scale; 1018 | } 1019 | public function add(string $left_operand, string $right_operand, int $scale = NULL): string { 1020 | $scale = $scale ?? $this->scale; 1021 | $ret = bcadd ( $left_operand, $right_operand, $scale ); 1022 | return $this->bctrim ( $ret ); 1023 | } 1024 | public function comp(string $left_operand, string $right_operand, int $scale = NULL): int { 1025 | $scale = $scale ?? $this->scale; 1026 | $ret = bccomp ( $left_operand, $right_operand, $scale ); 1027 | return $ret; 1028 | } 1029 | public function div(string $left_operand, string $right_operand, int $scale = NULL): string { 1030 | $scale = $scale ?? $this->scale; 1031 | $right_operand = $this->bctrim ( trim ( $right_operand ) ); 1032 | if ($right_operand === '0') { 1033 | throw new DivisionByZeroError (); 1034 | } 1035 | $ret = bcdiv ( $left_operand, $right_operand, $scale ); 1036 | return $this->bctrim ( $ret ); 1037 | } 1038 | public function mod(string $left_operand, string $modulus): string { 1039 | $scale = $scale ?? $this->scale; 1040 | $modulus = $this->bctrim ( trim ( $modulus ) ); 1041 | if ($modulus === '0') { 1042 | // if there was a ModulusByZero error, i would use it 1043 | throw new DivisionByZeroError (); 1044 | } 1045 | $ret = bcmod ( $left_operand, $modulus ); 1046 | return $this->bctrim ( $ret ); 1047 | } 1048 | public function mul(string $left_operand, string $right_operand, int $scale = NULL): string { 1049 | $scale = $scale ?? $this->scale; 1050 | $ret = bcmul ( $left_operand, $right_operand, $scale ); 1051 | return $this->bctrim ( $ret ); 1052 | } 1053 | public function pow(string $left_operand, string $right_operand, int $scale = NULL): string { 1054 | $scale = $scale ?? $this->scale; 1055 | $ret = bcpow ( $left_operand, $right_operand, $scale ); 1056 | return $this->bctrim ( $ret ); 1057 | } 1058 | public function powmod(string $left_operand, string $right_operand, string $modulus, int $scale = NULL): string { 1059 | $scale = $scale ?? $this->scale; 1060 | $modulus = $this->bctrim ( trim ( $modulus ) ); 1061 | if ($modulus === '0') { 1062 | // if there was a ModulusByZero error, i would use it 1063 | throw new DivisionByZeroError (); 1064 | } 1065 | $ret = bcpowmod ( $left_operand, $modulus, $modulus, $scale ); 1066 | return $this->bctrim ( $ret ); 1067 | } 1068 | public function scale(int $scale): bool { 1069 | $this->scale = $scale; 1070 | return true; 1071 | } 1072 | public function sqrt(string $operand, int $scale = NULL) { 1073 | $scale = $scale ?? $this->scale; 1074 | if (bccomp ( $operand, '-1' ) !== - 1) { 1075 | throw new RangeException ( 'tried to get the square root of number below zero!' ); 1076 | } 1077 | $ret = bcsqrt ( $left_operand, $scale ); 1078 | return $this->bctrim ( $ret ); 1079 | } 1080 | public function sub(string $left_operand, string $right_operand, int $scale = NULL): string { 1081 | $scale = $scale ?? $this->scale; 1082 | $ret = bcsub ( $left_operand, $right_operand, $scale ); 1083 | return $this->bctrim ( $ret ); 1084 | } 1085 | public static function bctrim(string $str): string { 1086 | $str = trim ( $str ); 1087 | if (false === strpos ( $str, '.' )) { 1088 | return $str; 1089 | } 1090 | $str = rtrim ( $str, '0' ); 1091 | if ($str [strlen ( $str ) - 1] === '.') { 1092 | $str = substr ( $str, 0, - 1 ); 1093 | } 1094 | return $str; 1095 | } 1096 | } 1097 | 1098 | /** 1099 | * needInputVariables: easy way to require variables, give a http 400 Bad Request with good error reports on missing parameters, 1100 | * and cast the variables to the correct native php type (i use it with extract(needInputVariables(['mail_to'=>'email','i'=>'int','foo'=>'bool','bar','P'])) ) 1101 | * 1102 | * @param array $variables 1103 | * variables that you require. if key is numeric, any type is accepted, and name is taken from value, otherwise name is taken from key and type is taken from variable. 1104 | * @param string $inputSources 1105 | * G=$_GET P=$_POST C=$_COOKIE A=$argv (not yet implemented) X=$customSources, and variables are extracted in the order given here. 1106 | * @param array $customSources 1107 | * (otional) 1108 | * array of custom sources to look through - ignored unless $inputSources contains X. 1109 | * @throws \LogicException 1110 | * @throws \InvalidArgumentException 1111 | * @throws \RuntimeException 1112 | * @return array 1113 | */ 1114 | function needInputVariables(array $variables, string $inputSources = 'P', array $customSources = array(), bool $exceptionMode = false): array { 1115 | $ret = array (); 1116 | $errors = array (); 1117 | foreach ( $variables as $key => $type ) { 1118 | if (is_numeric ( $key )) { 1119 | $key = $type; 1120 | $type = ''; // anything 1121 | } 1122 | // X (Custom) 1123 | $found = false; 1124 | foreach ( str_split ( $inputSources ) as $source ) { 1125 | switch ($source) { 1126 | case 'G' : // $_GET 1127 | { 1128 | if (array_key_exists ( $key, $_GET )) { 1129 | $found = true; 1130 | $val = $_GET [$key]; 1131 | break 2; 1132 | } 1133 | break; 1134 | } 1135 | case 'P' : // $_POST 1136 | { 1137 | if (array_key_exists ( $key, $_POST )) { 1138 | $found = true; 1139 | $val = $_POST [$key]; 1140 | break 2; 1141 | } 1142 | break; 1143 | } 1144 | case 'C' : // $_COOKIE 1145 | { 1146 | if (array_key_exists ( $key, $_COOKIE )) { 1147 | $found = true; 1148 | $val = $_COOKIE [$key]; 1149 | break 2; 1150 | } 1151 | break; 1152 | } 1153 | case 'A' : // $argv 1154 | { 1155 | throw new \LogicException ( 'FIXME: $argv NOT YET IMPLEMENTED' ); 1156 | } 1157 | case 'X' : // $customSources 1158 | { 1159 | foreach ( $customSources as $customSource ) { 1160 | if (array_key_exists ( $key, $customSource )) { 1161 | $found = true; 1162 | $val = $customSource [$key]; 1163 | break 3; 1164 | } 1165 | } 1166 | break; 1167 | } 1168 | default : 1169 | { 1170 | throw new \InvalidArgumentException ( 'unknown input source: ' . hhb_return_var_dump ( $source ) ); 1171 | } 1172 | } 1173 | } 1174 | 1175 | if (! $found) { 1176 | $errors [] = 'missing parameter: ' . $key; 1177 | continue; 1178 | } 1179 | if ($type === '') { 1180 | // anything, pass 1181 | } elseif (substr ( $type, 0, 6 ) === 'string') { 1182 | if (! is_string ( $val )) { 1183 | $errors [] = 'following parameter is not a string: ' . $key; 1184 | continue; 1185 | } 1186 | $type = substr ( $type, 6 ); 1187 | if (strlen ( $type )) { 1188 | if ($type [0] !== '(') { 1189 | throw \InvalidArgumentException (); 1190 | } 1191 | preg_match ( '/(\d+)(?:\,(\d+))?/', $type, $matches ); 1192 | $c = count ( $matches ); 1193 | if ($c > 3) { 1194 | throw new \InvalidArgumentException (); 1195 | } 1196 | if ($c > 2) { 1197 | $maxLen = $matches [2]; 1198 | if (strlen ( $val ) > $maxLen) { 1199 | $errors [] = 'following parameter cannot be longer than ' . $maxLen . ' byte(s): ' . $key; 1200 | continue; 1201 | } 1202 | } 1203 | if ($c > 1) { 1204 | $minLen = $matches [1]; 1205 | if (strlen ( $val ) < $minLen) { 1206 | $errors [] = 'following parameter must be at least ' . $minLen . ' byte(s): ' . $key; 1207 | continue; 1208 | } 1209 | } 1210 | } 1211 | } elseif ($type === 'bool') { 1212 | $val = filter_var ( $val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); 1213 | if (NULL === $val) { 1214 | $errors [] = 'following parameter is not a bool: ' . $key; 1215 | } 1216 | } elseif ($type === 'int' || $type === 'integer') { 1217 | $val = filter_var ( $val, FILTER_VALIDATE_INT ); 1218 | if (false === $val) { 1219 | $errors [] = 'following parameter is not a integer: ' . $key; 1220 | } 1221 | } elseif ($type === 'float' || $type === 'double') { 1222 | $val = filter_var ( $val, FILTER_VALIDATE_FLOAT ); 1223 | if (false === $val) { 1224 | $errors [] = 'following parameter is not a float: ' . $key; 1225 | } 1226 | } elseif ($type === 'email') { 1227 | $val = filter_var ( $val, FILTER_VALIDATE_EMAIL, (defined ( 'FILTER_FLAG_EMAIL_UNICODE' ) ? FILTER_FLAG_EMAIL_UNICODE : 0) ); 1228 | if (false === $val) { 1229 | $errors [] = 'following parameter is not an email: ' . $key; 1230 | } 1231 | } elseif ($type === 'ip') { 1232 | $val = filter_var ( $val, FILTER_VALIDATE_IP ); 1233 | if (false === $val) { 1234 | $errors [] = 'following parameter is not an ip address: ' . $key; 1235 | } 1236 | } elseif (is_callable ( $type )) { 1237 | $req = (new ReflectionFunction ( $type ))->getNumberOfRequiredParameters (); 1238 | if ($req === 1) { 1239 | $ret [$key] = $type ( $val ); 1240 | } elseif ($req === 2) { 1241 | $errstr = ''; 1242 | $ret [$key] = $type ( $val, $errstr ); 1243 | if (! empty ( $errstr )) { 1244 | $error [] = "parameter \"$key\": $errstr"; 1245 | } 1246 | } else { 1247 | throw new \InvalidArgumentException ( "callback validator must accept 1 or 2 parameters, but accepts \"$req\" parameters. (\$input[,&\$errorDescription]){...return \$input}" ); 1248 | } 1249 | continue; 1250 | } else { 1251 | throw new \InvalidArgumentException ( 'unsupported type: ' . hhb_return_var_dump ( $type ) ); 1252 | } 1253 | $ret [$key] = $val; 1254 | } 1255 | if (empty ( $errors )) { 1256 | return $ret; 1257 | } 1258 | $errstr = json_encode ( $errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | (defined ( 'JSON_UNESCAPED_LINE_TERMINATORS' ) ? JSON_UNESCAPED_LINE_TERMINATORS : 0) ); 1259 | if ($exceptionMode) { 1260 | throw new \RuntimeException ( $errstr ); 1261 | } 1262 | http_response_code ( 400 ); 1263 | header ( "content-type: text/plain;charset=utf8" ); 1264 | echo "HTTP 400 Bad Request: following errors were found: \n"; 1265 | die ( $errstr ); 1266 | } 1267 | -------------------------------------------------------------------------------- /libs/hhb_datatypes.inc.php: -------------------------------------------------------------------------------- 1 | 0 ) { 60 | if ($len < 2) { 61 | throw new Exception ( 'Invalid Nagle algorithm: at byte ' . $pos . ', length header is <2 bytes long!' ); 62 | } 63 | $sublen = from_little_uint16_t ( $nagled_binary [0] . $nagled_binary [1] ); 64 | $nagled_binary = substr ( $nagled_binary, 2 ); 65 | $len -= 2; 66 | if ($len < $sublen) { 67 | throw new Exception ( 'Invalid Nagle algorithm: length header at byte ' . $pos . ' specify a length of ' . $sublen . ' bytes, but only ' . $len . ' bytes remain!' ); 68 | } 69 | $ret [] = substr ( $nagled_binary, 0, $sublen ); 70 | $nagled_binary = substr ( $nagled_binary, $sublen ); 71 | $len -= $sublen; 72 | $pos += $sublen + 2; 73 | } 74 | return $ret; 75 | } 76 | -------------------------------------------------------------------------------- /research/7.6.tibia_client.class.php: -------------------------------------------------------------------------------- 1 | internal = new Tibia_client_internal($host, $port, $account, $password, $charname, $debugging); 15 | $this->internal->tibia_client = $this; 16 | } 17 | function __destruct() 18 | { 19 | unset($this->internal); // trying to force it to destruct now, this would be the appropriate time. 20 | } 21 | /** 22 | * ping the server 23 | * important to do this periodically, because if you don't, the server 24 | * will consider the connection broken, and kick you! 25 | * 26 | * @return void 27 | */ 28 | public function ping(): void 29 | { 30 | $this->internal->ping(); 31 | } 32 | const TALKTYPE_SAY = 1; 33 | const TALKTYPE_WHISPER = 2; 34 | const TALKTYPE_YELL = 3; 35 | const TALKTYPE_BROADCAST = 13; 36 | const TALKTYPE_MONSTER_SAY = 36; 37 | const TALKTYPE_MONSTER_YELL = 37; 38 | public function say(string $message, int $type = self::TALKTYPE_SAY): void 39 | { 40 | if (strlen($message) > 255) { 41 | throw new InvalidArgumentException( 42 | "message cannot be longer than 255 bytes! (PS: this is not a tibia protocol limitation, but a TFS limitation, " . 43 | "the protocol limitation is actually close to 65535 bytes.)" 44 | ); 45 | } 46 | if ($type < 0 || $type > 255) { 47 | throw new \InvalidArgumentException( 48 | "type must be between 0-255! " . 49 | "(also it can't be private-message or channel-message talk type but i cba writing the code to detect it right now)" 50 | ); 51 | } 52 | $packet = new Tibia_binary_serializer("\x96"); // 0x96: talk packet 53 | $packet->addU8($type); 54 | $packet->add_string($message); 55 | $this->internal->send($packet->str()); 56 | } 57 | // alias of walk_north 58 | public function walk_up(int $steps = 1): void 59 | { 60 | $this->walk_north($steps); 61 | } 62 | public function walk_north(int $steps = 1): void 63 | { 64 | //todo: invalidargumentexception < 0 65 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 66 | for ($i = 0; $i < $steps; ++$i) { 67 | $this->internal->send("\x65"); 68 | } 69 | } 70 | // alias of walk_east 71 | public function walk_right(int $steps = 1): void 72 | { 73 | $this->walk_east($steps); 74 | } 75 | public function walk_east(int $steps = 1): void 76 | { 77 | //todo: invalidargumentexception < 0 78 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 79 | for ($i = 0; $i < $steps; ++$i) { 80 | $this->internal->send("\x66"); 81 | } 82 | } 83 | // alias of walk_south 84 | public function walk_down(int $steps = 1): void 85 | { 86 | $this->walk_south($steps); 87 | } 88 | public function walk_south(int $steps = 1): void 89 | { 90 | //todo: invalidargumentexception < 0 91 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 92 | for ($i = 0; $i < $steps; ++$i) { 93 | $this->internal->send("\x67"); 94 | } 95 | } 96 | // alias of walk_west 97 | public function walk_left(int $steps = 1): void 98 | { 99 | $this->walk_west($steps); 100 | } 101 | public function walk_west(int $steps = 1): void 102 | { 103 | //todo: invalidargumentexception < 0 104 | //optimization note: steps can be concatenated nagle-style and issued in a single packet 105 | for ($i = 0; $i < $steps; ++$i) { 106 | $this->internal->send("\x68"); 107 | } 108 | } 109 | public function dance(int $moves = 10, int $msleep = 100) 110 | { 111 | // case 0x6F: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_NORTH); break; 112 | // case 0x70: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_EAST); break; 113 | // case 0x71: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_SOUTH); break; 114 | // case 0x72: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_WEST); break; 115 | $direction_bytes = "\x6F\x70\x71\x72"; 116 | $blen = strlen($direction_bytes) - 1; 117 | $last = null; 118 | for ($i = 0; $i < $moves; ++$i) { 119 | do { 120 | $neww = rand(0, $blen); 121 | } while ($neww === $last); 122 | $last = $neww; 123 | $this->internal->send($direction_bytes[$neww]); 124 | usleep($msleep * 1000); 125 | } 126 | } 127 | } 128 | class Tibia_client_internal 129 | { 130 | const TIBIA_VERSION_INT = 760; 131 | const TIBIA_VERSION_STRING = "7.6"; 132 | // CLIENTOS_WINDOWS - it would be a major task to actually support emulating different OSs, they have different login protocols, 133 | // so for simplicity, we always say we're the Windows client. 134 | const TIBIA_CLIENT_OS_INT = 4; 135 | const TIBIA_CLIENT_OS_STRING = 'CLIENTOS_WINDOWS'; 136 | /** @var Tibia_client|NULL $tibia_client */ 137 | public $tibia_client; 138 | protected $public_key_parsed_cache = null; 139 | public $debugging = false; 140 | protected $ip; 141 | protected $port; 142 | protected $account; 143 | protected $password; 144 | public $charname; 145 | protected $socket; 146 | function __construct(string $host, int $port, int $account, string $password, string $charname, bool $debugging = false) 147 | { 148 | if ($account < 0) { 149 | throw new \InvalidArgumentException("account MUST be >= 0 (and <= 0xFFFFFFFF)"); 150 | } 151 | if ($account > 0xFFFFFFFF) { 152 | throw new \InvalidArgumentException("account MUST be <= 0xFFFFFFFF (and >= 0)"); 153 | } 154 | $ip = $host; 155 | if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 156 | $ip = gethostbyname($ip); 157 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) { 158 | throw new \RuntimeException("failed to get ip of hostname {$host}"); 159 | } 160 | if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 161 | throw new \RuntimeException("could only find an ipv6 address for that host, ipv6 support is (not yet?) implemented!"); 162 | } 163 | } 164 | $this->ip = $ip; 165 | $this->port = $port; 166 | $this->account = $account; 167 | $this->password = $password; 168 | $this->charname = $charname; 169 | $this->debugging = $debugging; 170 | $this->login(); 171 | } 172 | function __destruct() 173 | { 174 | $this->logout(); 175 | } 176 | protected function login(): void 177 | { 178 | if (!!$this->socket) { 179 | throw new \LogicException("socket already initialized during login()! "); 180 | } 181 | $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 182 | if (false === $this->socket) { 183 | $err = socket_last_error(); 184 | throw new \RuntimeException("socket_create(AF_INET, SOCK_STREAM, SOL_TCP) failed! {$err}: " . socket_strerror($err)); 185 | } 186 | if (!socket_set_block($this->socket)) { 187 | $err = socket_last_error($this->socket); 188 | throw new \RuntimeException("socket_set_block() failed! {$err}: " . socket_strerror($err)); 189 | } 190 | if (!socket_connect($this->socket, $this->ip, $this->port)) { 191 | $err = socket_last_error($this->socket); 192 | throw new \RuntimeException("socket_connect() failed! {$err}: " . socket_strerror($err)); 193 | } 194 | if (!socket_set_option($this->socket, SOL_TCP, TCP_NODELAY, 1)) { 195 | // this actually avoids some bugs, espcially if you try to talk right after login, 196 | // won't work with TCP_NODELAY disabled, but will work with TCP_NODELAY enabled. 197 | // (why? not sure.) 198 | $err = socket_last_error($this->socket); 199 | throw new \RuntimeException("setting TCP_NODELAY failed! {$err}: " . socket_strerror($err)); 200 | } { 201 | $packet = new Tibia_binary_serializer(); 202 | $packet->addU16(522); // 522: game-server connection 203 | $packet->addU8($this::TIBIA_CLIENT_OS_INT); 204 | $packet->addU16($this::TIBIA_VERSION_INT); 205 | $packet->addU8(0); // gamemaster flag, probably? 206 | $packet->addU32($this->account); 207 | $packet->add_string($this->charname); 208 | $packet->add_string($this->password); 209 | 210 | $this->send($packet->str(), true); 211 | // if we don't sleep a little after logging in, nothing will work, talking, walking, etc won't respond for the first 212 | // few milliseconds or so. (???) 213 | usleep(100 * 1000); 214 | //$this->ping(); // because why not.. 215 | } 216 | } 217 | /** 218 | * read next packet 219 | * if $wait_for_packet is false and no packet is available, NULL is returned. 220 | * if $remove_size_header is false, a 0-byte packet (packet only having a size header for 0 bytes) will result in an empty string. (ping packet? TCP_KEEPALIVE packet?) 221 | * 222 | * @param boolean $wait_for_packet 223 | * @param boolean $remove_size_header 224 | * @return string|null 225 | */ 226 | public function read_next_packet(bool $wait_for_packet, bool $remove_size_header = true): ?string 227 | { 228 | $flag = ($wait_for_packet ? MSG_WAITALL : MSG_DONTWAIT); 229 | $read = ''; 230 | $buf = ''; 231 | // 2 bytes: tibia packet size header, little-endian uint16 232 | $ret = socket_recv($this->socket, $buf, 2, $flag); 233 | if ($ret === 0 || ($ret === false && socket_last_error($this->socket) === SOCKET_EWOULDBLOCK)) { 234 | // no new packet available 235 | if (!$wait_for_packet) { 236 | // .. and we're not waiting. 237 | return null; 238 | } 239 | // FIXME socket_recv timed out even with MSG_WAITALL (it's a socksetopt option to change the timeout) 240 | return null; 241 | } 242 | if ($ret === false) { 243 | // ps: recv error at this stage probably did not corrupt the recv buffer. (unlike in the rest of this function) 244 | $erri = socket_last_error($this->socket); 245 | $err = socket_strerror($erri); 246 | throw new \RuntimeException("socket_recv error {$erri}: {$err}"); 247 | } 248 | assert(strlen($buf) >= 1); 249 | $read .= $buf; 250 | $buf = ''; 251 | if ($ret === 1) { 252 | // ... we have HALF a size header, wait for the other half regardless of $wait_for_packet (it should come ASAP anyway) 253 | // (if we don't, then the buffer is in a corrupt state where next read_next_packet will read half a size header! 254 | // - another way to handle this would be to use MSG_PEEK but oh well) 255 | $ret = socket_recv($this->socket, $buf, 1, MSG_WAITALL); 256 | if ($ret === false) { 257 | $erri = socket_last_error($this->socket); 258 | $err = socket_strerror($erri); 259 | throw new \RuntimeException("socket_recv error {$erri}: {$err} - also: the recv buffer is now in a corrupted state, " . 260 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 261 | "or a bugged server or something)"); 262 | } 263 | if ($ret !== 1) { 264 | throw new \RuntimeException("even with MSG_WAITALL we could only read half a size header! the recv buffer is now in a corrupted state, " . 265 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 266 | "or a bugged server or something)"); 267 | } 268 | assert(1 === strlen($buf)); 269 | $read .= $buf; 270 | $buf = ''; 271 | } 272 | assert(2 === strlen($read)); 273 | assert(0 === strlen($buf)); 274 | $size = from_little_uint16_t($read); 275 | while (0 < ($remaining = (($size + 2) - strlen($read)))) { 276 | $buf = ''; 277 | $ret = socket_recv($this->socket, $buf, $remaining, MSG_WAITALL); 278 | if ($ret === false) { 279 | $erri = socket_last_error($this->socket); 280 | $err = socket_strerror($erri); 281 | throw new \RuntimeException("socket_recv error {$erri}: {$err} - also: the recv buffer is now in a corrupted state, " . 282 | "you should throw away this instance of TibiaClient and re-login (this should never happen btw, you probably have a very unstable connection " . 283 | "or a bugged server or something)"); 284 | } 285 | if (0 === $ret) { 286 | throw new \RuntimeException("even with MSG_WAITALL and trying to read {$remaining} bytes, socket_recv return 0! something is very wrong. " . 287 | "also the recv buffer is now in a corrupted state, you should throw away this instance of TibiaClient and re-login. " . 288 | "(this should never happen btw, you probably have a very unstable connection " . 289 | "or a bugged server or something)"); 290 | } 291 | $read .= $buf; 292 | } 293 | if ($remaining !== 0) { 294 | throw new \LogicException("...wtf, after the read loop, remaining was: " . hhb_return_var_dump($remaining) . " - should never happen, probably a code bug."); 295 | } 296 | if (strlen($read) !== ($size + 2)) { 297 | throw new \LogicException('...wtf, `strlen($read) === ($size + 2)` sanity check failed, should never happen, probably a code bug.'); 298 | } 299 | assert(strlen($read) >= 2); 300 | if ($remove_size_header) { 301 | $read = substr($read, 2); 302 | } 303 | return $read; 304 | } 305 | /** 306 | * ping the server 307 | * important to do this periodically, because if you don't, the server 308 | * will consider the connection broken, and kick you! 309 | * 310 | * @return void 311 | */ 312 | public function ping(): void 313 | { 314 | $this->send("\x1E"); 315 | } 316 | /** 317 | * parse tibia_str 318 | * if it is a valid tibia_str, returns the tibia str, length header and trailing bytes removed. 319 | * if it's *not* a valid tibia_str, returns null 320 | * a tibia_str may be binary. 321 | * 322 | * @param string $bytes 323 | * @param integer $offset 324 | * @return string|null 325 | */ 326 | public static function parse_tibia_str(string $bytes): ?string 327 | { 328 | $len = strlen($bytes); 329 | if ($len < 2) { 330 | // not a tibia_str. 331 | return null; 332 | } 333 | $claimed_len = from_little_uint16_t(substr($bytes, 0, 2)); 334 | if ($len < ($claimed_len + 2)) { 335 | // not a tibia_str. 336 | return null; 337 | } 338 | // valid tibia_str. (even if it has trailing bytes, which are ignored.) 339 | $ret = substr($bytes, 2, $claimed_len); 340 | return $ret; 341 | } 342 | const POSITION_SIZE_BYTES = 5; 343 | public static function parse_position(string $bytes): ?array 344 | { 345 | $len = strlen($bytes); 346 | if ($len < self::POSITION_SIZE_BYTES) { 347 | return null; 348 | } 349 | $ret = array(); 350 | $ret['x'] = from_little_uint16_t(substr($bytes, 0, 2)); 351 | $ret['y'] = from_little_uint16_t(substr($bytes, 2, 2)); 352 | $ret['z'] = from_uint8_t(substr($bytes, 4, 1)); 353 | return $ret; 354 | } 355 | public function tibia_str(string $str): string 356 | { 357 | $len = strlen($str); 358 | if ($len > 65535) { 359 | throw new OutOfRangeException('max length of a tibia_str is 65535 bytes! (i think..)'); 360 | } 361 | return to_little_uint16_t($len) . $str; 362 | } 363 | protected $is_logged_out = false; 364 | public function logout_force() 365 | { 366 | $this->logout(); 367 | } 368 | protected function logout(): void 369 | { 370 | if ($this->is_logged_out) { 371 | return; 372 | } 373 | $this->is_logged_out = true; 374 | try { 375 | $this->send("\x14"); 376 | // TFS bug? if we send the disconnect request too fast before closing the socket, 377 | // the server will not log out the actual avatar.. 378 | //usleep(50000*1000); 379 | while ($this->read_next_packet(false, false) !== null) { 380 | //... 381 | } 382 | $this->send("\x0F"); 383 | usleep(100 * 1000); 384 | } finally { 385 | if ($this->socket) { 386 | socket_close($this->socket); 387 | } 388 | } 389 | } 390 | public function send(string $packet, bool $add_size_header = true): void 391 | { 392 | if ($add_size_header) { 393 | $len = strlen($packet); 394 | if ($len > 65535) { 395 | // note that it's still possible to have several separate packets each individually under 65535 bytes, 396 | // concantenated with the Nagle-algorithm but then you have to add the size headers and adler checksums manually, 397 | // before calling send() 398 | throw new OutOfRangeException('Cannot automatically add size header a to a packet above 65535 bytes!'); 399 | } 400 | $packet = to_little_uint16_t($len) . $packet; 401 | } 402 | $this->socket_write_all($this->socket, $packet); 403 | } 404 | /** 405 | * writes ALL data to socket, and throws an exception if that's not possible. 406 | * 407 | * @param socket $socket 408 | * @param string $data 409 | * @return void 410 | */ 411 | public static function socket_write_all($socket, string $data): void 412 | { 413 | if (!($dlen = strlen($data))) { 414 | return; 415 | } 416 | do { 417 | assert($dlen > 0); 418 | assert(strlen($data) === $dlen); 419 | $sent_now = socket_write($socket, $data); 420 | if (false === $sent_now) { 421 | $err = socket_last_error($socket); 422 | throw new \RuntimeException("socket_write() failed! {$err}: " . socket_strerror($err)); 423 | } 424 | if (0 === $sent_now) { 425 | // we'll try *1* last time before throwing exception... 426 | $sent_now = socket_write($socket, $data); 427 | if (false === $sent_now) { 428 | $err = socket_last_error($socket); 429 | throw new \RuntimeException("socket_write() failed after first returning zero! {$err}: " . socket_strerror($err)); 430 | } 431 | if (0 === $sent_now) { 432 | // something is very wrong but it's not registering as an error at the kernel apis... 433 | throw new \RuntimeException("socket_write() keeps returning 0 bytes sent while {$dlen} byte(s) to send!"); 434 | } 435 | } 436 | $dlen -= $sent_now; 437 | $data = substr($data, $sent_now); 438 | } while ($dlen > 0); 439 | assert($dlen === 0); 440 | assert(strlen($data) === 0); 441 | // all data sent. 442 | return; 443 | } 444 | public static function parse_packet(string $packet, bool $size_header_removed = true): Tibia_client_packet_parsed 445 | { 446 | // for now i cba writing stuff to handle size header / adler checksum / xtea encryption in here... 447 | if (!$size_header_removed) { 448 | throw new \InvalidArgumentException("remove size header before calling this function."); 449 | } 450 | $ret = new Tibia_client_packet_parsed(); 451 | $ret->bytes_hex = bin2hex($packet); 452 | $len = strlen($packet); 453 | if ($len === 0) { 454 | // uhhh.... 455 | $ret->type = 0; 456 | $ret->type_name = "ping_0_bytes"; // ping_tcp_keepalive ? 457 | return $ret; 458 | } 459 | $ret->type = from_uint8_t(substr($packet, 0, 1)); 460 | $packet = substr($packet, 1); 461 | switch ($ret->type) { 462 | case 0x0D: { 463 | // seems to be either ping or ping_request (eg a request that we ping back) 464 | $ret->type_name = "ping_0x0D"; 465 | return $ret; 466 | break; 467 | } 468 | case 0x17: { 469 | // TODO: better parsing of this packet, which is a big task (this packet is very very complex for some reason.) 470 | $ret->type_name = "login_and_map_and_welcome"; 471 | $welcome_messages = []; 472 | $found = 0; 473 | $ret->data['welcome_messages'] = []; 474 | for ($i = strlen($packet); $i > 0; --$i) { 475 | $str = Tibia_client_internal::parse_tibia_str(substr($packet, $i)); 476 | if (null === $str) { 477 | continue; 478 | } 479 | if (strlen($str) < 1) { 480 | continue; 481 | } 482 | if (strlen($str) !== strcspn($str, "\x00\x01")) { 483 | continue; 484 | } 485 | // PROBABLY found the message. 486 | ++$found; 487 | $ret->data['welcome_messages'][] = ($str); 488 | if ($found >= 2) { 489 | break; 490 | } 491 | } 492 | return $ret; 493 | break; 494 | } 495 | case Tibia_client_packet_parsed::TYPE_SAY: // 0xAA 496 | { 497 | $ret->type_name = "TYPE_SAY"; 498 | // This serializer will do packet parse cleanups for us 499 | $sub_packet = new Tibia_binary_serializer($packet); 500 | $ret->data['speaker_name'] = $sub_packet->get_string(); 501 | $ret->data['speak_type'] = $sub_packet->getU8(); 502 | $ret->data['speaker_position'] = $sub_packet->get_position(); 503 | $ret->data['text'] = $sub_packet->get_string(); 504 | // Tell packet parser that your done, 505 | // if it disagrees with you, there is still data in packet. 506 | // And it will give you a warning 507 | if (!strlen($sub_packet->str())) { 508 | $ret->warnings[] = "extra bytes: " . bin2hex($sub_packet->str()); 509 | } 510 | return $ret; 511 | unset($strlen); 512 | break; 513 | } 514 | default: { 515 | $ret->type_name = "unknown 0x" . bin2hex(to_uint8_t($ret->type)); 516 | return $ret; 517 | break; 518 | } 519 | } 520 | // ...unreachable? 521 | return $ret; 522 | } 523 | } 524 | class Tibia_client_packet_parsed 525 | { 526 | const TYPE_SAY = 0xAA; 527 | /** @var u8 $type */ 528 | public $type; 529 | /** @var string $type_name */ 530 | public $type_name = "unknown"; 531 | public $size_header_removed = true; 532 | public $bytes_hex = ""; 533 | public $data = []; 534 | public $errors = []; 535 | public $warnings = []; 536 | } 537 | -------------------------------------------------------------------------------- /research/Player.class.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 21 | $this->port = $port; 22 | $this->acc = $acc; 23 | $this->password = $password; 24 | $this->charname = $charname; 25 | $this->login(); 26 | } 27 | function __destruct() 28 | { 29 | if (is_resource($this->socket)) { 30 | $snd = static::hex('01 00 14'); 31 | @socket_send($this->socket, $snd, strlen($snd), 0); 32 | @ex::socket_shutdown($this->socket, 2); 33 | ex::socket_close($this->socket); 34 | } 35 | } 36 | public function send(string $packet, bool $autoheader = true) 37 | { 38 | if ($autoheader) { 39 | $len = strlen($packet); 40 | if ($len > 65535) { 41 | throw new OutOfRangeException('Cannot autoheader a packet above 65535 bytes!'); 42 | } 43 | $packet = to_little_uint16_t($len) . $packet; 44 | unset($len); 45 | } 46 | ex::socket_send($this->socket, $packet, strlen($packet), 0); 47 | } 48 | private function tibiastr(string $str) : string 49 | { 50 | $len = strlen($str); 51 | if ($len > 65535) { 52 | throw new OutOfRangeException('max length of a tibiastring is 65535 bytes! (i think..)'); 53 | } 54 | return to_little_uint16_t($len) . $str; 55 | } 56 | public function walkSouth(int $steps = 1) 57 | { 58 | $packet = static::hex('01 00 67'); 59 | $packet = str_repeat($packet, $steps); 60 | $this->send($packet, false); 61 | } 62 | public function walkDown(int $steps = 1) 63 | { 64 | $packet = static::hex('01 00 67'); 65 | $packet = str_repeat($packet, $steps); 66 | $this->send($packet, false); 67 | } 68 | public function walkLeft(int $steps = 1) 69 | { 70 | $packet = static::hex('01 00 68'); 71 | $packet = str_repeat($packet, $steps); 72 | $this->send($packet, false); 73 | } 74 | public function walkRight(int $steps = 1) 75 | { 76 | $packet = static::hex('01 00 66'); 77 | $packet = str_repeat($packet, $steps); 78 | $this->send($packet, false); 79 | } 80 | public function walkUp(int $steps = 1) 81 | { 82 | $packet = static::hex('01 00 65'); 83 | $packet = str_repeat($packet, $steps); 84 | $this->send($packet, false); 85 | } 86 | public function say(string $msg) 87 | { 88 | $packet = static::hex('9601') . $this->tibiastr($msg); 89 | $this->send($packet); 90 | } 91 | private function login() 92 | { 93 | $this->socket = ex::socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 94 | ex::socket_set_block($this->socket); 95 | // contrary to PHP docs (saying that bind should be done before connect), OS is supposed to do this automatically: while(!socket_bind ( $this->socket, '0.0.0.0', mt_rand ( 1024, 5000 ) )); 96 | ex::socket_connect($this->socket, $this->ip, $this->port); 97 | $packet = static::hex('0A 03 37 F2 FF 07 00 66 75 63 6B 79 6F 75 19 00 78 58 35 38 34 38 4A 67 6A 72 49 45 70 6F 77 6F 4B 46 6B 66 72 69 72 47 4A 1C 00 6A 51 30 74 43 2F 71 51 65 68 6A 56 49 36 51 79 48 75 45 63 65 6B 67 49 31 70 6B 3D F6 02 00 '); 98 | $packet .= to_little_uint32_t($this->acc); 99 | $packet .= $this->tibiastr($this->charname); 100 | $packet .= $this->tibiastr($this->password); 101 | $packet .= static::hex('00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'); 102 | // ???^ 103 | $this->send($packet, true); 104 | $packet = static::hex('0400A00200010300814500030081450003008145000300814500'); 105 | // ???^ 106 | $this->send($packet, true); 107 | $buf = ''; 108 | // ex::socket_recv ( $this->socket, $buf, 999, 0 ); 109 | // hhb_var_dump ( ($buf) ); 110 | } 111 | static function hex(string $hexstr) : string 112 | { 113 | $ret = trim(str_replace(' ', '', $hexstr)); 114 | return hex2bin($ret); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /research/searchBinary.php: -------------------------------------------------------------------------------- 1 | 0) { 14 | try { 15 | $decrypted = XTEA::decrypt(pad($bin, 8), $xtea_key_array, 32); 16 | } catch (Throwable $ex) { 17 | /// 18 | $decrypted = ''; 19 | } 20 | echo "{$i}: "; 21 | echo " uint8: " . from_uint8_t(substr($bin, 0, 2)); 22 | if ($len >= 2) { 23 | echo " uint16: " . from_little_uint16_t(substr($bin, 0, 2)); 24 | if ($len >= 4) { 25 | echo " uint32: " . from_little_uint32_t(substr($bin, 0, 4)); 26 | } 27 | } 28 | echo " decrypted: "; 29 | 30 | $ipos = stripos($decrypted, 'anal'); 31 | if (false !== $ipos) { 32 | echo ("!!!!!!!!!!!!!!!!!!!!!!!!!! "); 33 | var_dump($decrypted); 34 | } else { 35 | var_dump($ipos); 36 | } 37 | //echo "\n"; 38 | ++$i; 39 | $bin = substr($bin, 1); 40 | } 41 | 42 | function pad(string $data, int $multiple) 43 | { 44 | $len = strlen($data); 45 | if ((($len % $multiple) !== 0)) { 46 | $nearest = (int)(ceil($len / $multiple) * $multiple); 47 | assert($nearest !== $len); 48 | assert($nearest > $len); 49 | $data .= str_repeat("\x00", $nearest - $len); 50 | $len = $nearest; 51 | } 52 | return $data; 53 | } 54 | 55 | function to_uint8_t(int $i) : string 56 | { 57 | return pack('C', $i); 58 | } 59 | function from_uint8_t(string $i) : int 60 | { 61 | // ord($i) , i know. 62 | $arr = unpack("Cuint8_t", $i); 63 | return $arr['uint8_t']; 64 | } 65 | function from_little_uint16_t(string $i) : int 66 | { 67 | $arr = unpack('vuint16_t', $i); 68 | return $arr['uint16_t']; 69 | } 70 | function from_big_uint16_t(string $i) : int 71 | { 72 | $arr = unpack('nuint16_t', $i); 73 | return $arr['nint16_t']; 74 | } 75 | function to_little_uint16_t(int $i) : string 76 | { 77 | return pack('v', $i); 78 | } 79 | function to_big_uint16_t(int $i) : string 80 | { 81 | return pack('n', $i); 82 | } 83 | function from_little_uint32_t(string $i) : int 84 | { 85 | $arr = unpack('Vuint32_t', $i); 86 | return $arr['uint32_t']; 87 | } 88 | function from_big_uint32_t(string $i) : int 89 | { 90 | $arr = unpack('Nuint32_t', $i); 91 | return $arr['uint32_t']; 92 | } 93 | function to_little_uint32_t(int $i) : string 94 | { 95 | return pack('V', $i); 96 | } 97 | function to_big_uint32_t(int $i) : string 98 | { 99 | return pack('N', $i); 100 | } 101 | function from_little_uint64_t(string $i) : int 102 | { 103 | $arr = unpack('Puint64_t', $i); 104 | return $arr['uint64_t']; 105 | } 106 | function from_big_uint64_t(string $i) : int 107 | { 108 | $arr = unpack('Juint64_t', $i); 109 | return $arr['uint64_t']; 110 | } 111 | function to_little_uint64_t(int $i) : string 112 | { 113 | return pack('P', $i); 114 | } 115 | function to_big_uint64_t(int $i) : string 116 | { 117 | return pack('J', $i); 118 | } 119 | /* splits up Nagle Algorithm combined data */ 120 | function hhb_denagle(string $nagled_binary) : array 121 | { 122 | $ret = array(); 123 | $pos = 0; 124 | $len = strlen($nagled_binary); 125 | while ($len > 0) { 126 | if ($len < 2) { 127 | throw new Exception('Invalid Nagle algorithm: at byte ' . $pos . ', length header is <2 bytes long!'); 128 | } 129 | $sublen = from_little_uint16_t($nagled_binary[0] . $nagled_binary[1]); 130 | $nagled_binary = substr($nagled_binary, 2); 131 | $len -= 2; 132 | if ($len < $sublen) { 133 | throw new Exception('Invalid Nagle algorithm: length header at byte ' . $pos . ' specify a length of ' . $sublen . ' bytes, but only ' . $len . ' bytes remain!'); 134 | } 135 | $ret[] = substr($nagled_binary, 0, $sublen); 136 | $nagled_binary = substr($nagled_binary, $sublen); 137 | $len -= $sublen; 138 | $pos += $sublen + 2; 139 | } 140 | return $ret; 141 | } -------------------------------------------------------------------------------- /research/xtea2.php: -------------------------------------------------------------------------------- 1 | | 6 | // +----------------------------------------------------------------------+ 7 | // | Original code: http://vader.brad.ac.uk/tea/source.shtml#new_ansi | 8 | // | Currently to be found at: | 9 | // | http://www.simonshepherd.supanet.com/source.shtml#new_ansi | 10 | // +----------------------------------------------------------------------+ 11 | // 12 | // $Id$ 13 | // 14 | /** 15 | * Class that implements the xTEA encryption algorithm. 16 | * Class that implements the xTEA encryption algorithm.
17 | * This enables you to encrypt data without requiring mcrypt. 18 | * 19 | * From the C source: 20 | * ----------------------------------------- 21 | * The Tiny Encryption Algorithm (TEA) by 22 | * David Wheeler and Roger Needham of the 23 | * Cambridge Computer Laboratory. 24 | * 25 | * Placed in the Public Domain by 26 | * David Wheeler and Roger Needham. 27 | * 28 | * **** ANSI C VERSION (New Variant) **** 29 | * 30 | * Notes: 31 | * 32 | * TEA is a Feistel cipher with XOR and 33 | * and addition as the non-linear mixing 34 | * functions. 35 | * 36 | * Takes 64 bits of data in v[0] and v[1]. 37 | * Returns 64 bits of data in w[0] and w[1]. 38 | * Takes 128 bits of key in k[0] - k[3]. 39 | * 40 | * TEA can be operated in any of the modes 41 | * of DES. Cipher Block Chaining is, for example, 42 | * simple to implement. 43 | * 44 | * n is the number of iterations. 32 is ample, 45 | * 16 is sufficient, as few as eight may be OK. 46 | * The algorithm achieves good dispersion after 47 | * six iterations. The iteration count can be 48 | * made variable if required. 49 | * 50 | * Note this is optimised for 32-bit CPUs with 51 | * fast shift capabilities. It can very easily 52 | * be ported to assembly language on most CPUs. 53 | * 54 | * delta is chosen to be the real part of (the 55 | * golden ratio Sqrt(5/4) - 1/2 ~ 0.618034 56 | * multiplied by 2^32). 57 | * 58 | * This version has been amended to foil two 59 | * weaknesses identified by David A. Wagner 60 | * (daw@cs.berkeley.edu): 1) effective key 61 | * length of old-variant TEA was 126 not 128 62 | * bits 2) a related key attack was possible 63 | * although impractical. 64 | * 65 | * void encipher(unsigned long *const v,unsigned long *const w, 66 | * const unsigned long *const k) 67 | * { 68 | * register unsigned long y=v[0],z=v[1],sum=0,delta=0x9E3779B9,n=32; 69 | * 70 | * while(n-->0) 71 | * { 72 | * y+= (z<<4 ^ z>>5) + z ^ sum + k[sum&3]; 73 | * sum += delta; 74 | * z+= (y<<4 ^ y>>5) + y ^ sum + k[sum>>11 & 3]; 75 | * } 76 | * 77 | * w[0]=y; w[1]=z; 78 | * } 79 | * 80 | * void decipher(unsigned long *const v,unsigned long *const w, 81 | * const unsigned long *const k) 82 | * { 83 | * register unsigned long y=v[0],z=v[1],sum=0xC6EF3720, 84 | * delta=0x9E3779B9,n=32; 85 | * 86 | * # sum = delta<<5, in general sum = delta * n 87 | * 88 | * while(n-->0) 89 | * { 90 | * z-= (y<<4 ^ y>>5) + y ^ sum + k[sum>>11 & 3]; 91 | * sum -= delta; 92 | * y-= (z<<4 ^ z>>5) + z ^ sum + k[sum&3]; 93 | * } 94 | * 95 | * w[0]=y; w[1]=z; 96 | * } 97 | * 98 | * ----------------------------------------- 99 | * 100 | * @TODO Add CFB. 101 | * 102 | * @package Crypt_Xtea 103 | * @version $Revision$ 104 | * @access public 105 | * @author Jeroen Derks 106 | */ 107 | class Crypt_Xtea 108 | { 109 | /** 110 | * Number of iterations. 111 | * @var integer 112 | * @access private 113 | * @see setIter(), getIter() 114 | */ 115 | var $n_iter; 116 | function __construct() 117 | { 118 | $this->setIter(32); 119 | } 120 | // {{{ setIter() 121 | /** 122 | * Set the number of iterations to use. 123 | * 124 | * @param integer $n_iter Number of iterations to use. 125 | * 126 | * @access public 127 | * @author Jeroen Derks 128 | * @see $n_iter, getIter() 129 | */ 130 | function setIter($n_iter) 131 | { 132 | $this->n_iter = $n_iter; 133 | } 134 | // }}} 135 | // {{{ getIter() 136 | /** 137 | * Get the number of iterations to use. 138 | * 139 | * @return integer Number of iterations to use. 140 | * 141 | * @access public 142 | * @author Jeroen Derks 143 | * @see $n_iter, setIter() 144 | */ 145 | function getIter() 146 | { 147 | return $this->n_iter; 148 | } 149 | // }}} 150 | // {{{ encrypt() 151 | /** 152 | * Encrypt a string using a specific key. 153 | * 154 | * @param string $data Data to encrypt. 155 | * @param string $key Key to encrypt data with (binary string). 156 | * 157 | * @return string Binary encrypted character string. 158 | * 159 | * @access public 160 | * @author Jeroen Derks 161 | * @see decrypt(), _encipherLong(), _resize(), _str2long() 162 | */ 163 | function encrypt(string $data, string $key) 164 | { 165 | // resize data to 32 bits (4 bytes) 166 | $n = $this->_resize($data, 4); 167 | // convert data to long 168 | $data_long[0] = $n; 169 | $n_data_long = $this->_str2long(1, $data, $data_long); 170 | // resize data_long to 64 bits (2 longs of 32 bits) 171 | $n = count($data_long); 172 | if (($n & 1) == 1) { 173 | $data_long[$n] = chr(0); 174 | $n_data_long++; 175 | } 176 | // resize key to a multiple of 128 bits (16 bytes) 177 | $this->_resize($key, 16, true); 178 | // convert key to long 179 | $n_key_long = $this->_str2long(0, $key, $key_long); 180 | // encrypt the long data with the key 181 | $enc_data = ''; 182 | $w = array(0, 0); 183 | $j = 0; 184 | $k = array(0, 0, 0, 0); 185 | for ($i = 0; $i < $n_data_long; ++$i) { 186 | // get next key part of 128 bits 187 | if ($j + 4 <= $n_key_long) { 188 | $k[0] = $key_long[$j]; 189 | $k[1] = $key_long[$j + 1]; 190 | $k[2] = $key_long[$j + 2]; 191 | $k[3] = $key_long[$j + 3]; 192 | } else { 193 | $k[0] = $key_long[$j % $n_key_long]; 194 | $k[1] = $key_long[($j + 1) % $n_key_long]; 195 | $k[2] = $key_long[($j + 2) % $n_key_long]; 196 | $k[3] = $key_long[($j + 3) % $n_key_long]; 197 | } 198 | $j = ($j + 4) % $n_key_long; 199 | $this->_encipherLong($data_long[$i], $data_long[++$i], $w, $k); 200 | // append the enciphered longs to the result 201 | $enc_data .= $this->_long2str($w[0]); 202 | $enc_data .= $this->_long2str($w[1]); 203 | } 204 | return $enc_data; 205 | } 206 | // }}} 207 | // {{{ decrypt() 208 | /** 209 | * Decrypt an encrypted string using a specific key. 210 | * 211 | * @param string $data Encrypted data to decrypt. 212 | * @param string $key Key to decrypt encrypted data with (binary string). 213 | * 214 | * @return string Binary decrypted character string. 215 | * 216 | * @access public 217 | * @author Jeroen Derks 218 | * @see _encipherLong(), encrypt(), _resize(), _str2long() 219 | */ 220 | function decrypt($enc_data, $key) 221 | { 222 | // convert data to long 223 | $n_enc_data_long = $this->_str2long(0, $enc_data, $enc_data_long); 224 | // resize key to a multiple of 128 bits (16 bytes) 225 | $this->_resize($key, 16, true); 226 | // convert key to long 227 | $n_key_long = $this->_str2long(0, $key, $key_long); 228 | // decrypt the long data with the key 229 | $data = ''; 230 | $w = array(0, 0); 231 | $j = 0; 232 | $len = 0; 233 | $k = array(0, 0, 0, 0); 234 | $pos = 0; 235 | for ($i = 0; $i < $n_enc_data_long; $i += 2) { 236 | // get next key part of 128 bits 237 | if ($j + 4 <= $n_key_long) { 238 | $k[0] = $key_long[$j]; 239 | $k[1] = $key_long[$j + 1]; 240 | $k[2] = $key_long[$j + 2]; 241 | $k[3] = $key_long[$j + 3]; 242 | } else { 243 | $k[0] = $key_long[$j % $n_key_long]; 244 | $k[1] = $key_long[($j + 1) % $n_key_long]; 245 | $k[2] = $key_long[($j + 2) % $n_key_long]; 246 | $k[3] = $key_long[($j + 3) % $n_key_long]; 247 | } 248 | $j = ($j + 4) % $n_key_long; 249 | $this->_decipherLong($enc_data_long[$i], $enc_data_long[$i + 1], $w, $k); 250 | 251 | // append the deciphered longs to the result data (remove padding) 252 | if (0 == $i) { 253 | $len = $w[0]; 254 | if (4 <= $len) { 255 | $data .= $this->_long2str($w[1]); 256 | } else { 257 | $data .= substr($this->_long2str($w[1]), 0, $len % 4); 258 | } 259 | } else { 260 | $pos = ($i - 1) * 4; 261 | if ($pos + 4 <= $len) { 262 | $data .= $this->_long2str($w[0]); 263 | if ($pos + 8 <= $len) { 264 | $data .= $this->_long2str($w[1]); 265 | } elseif ($pos + 4 < $len) { 266 | $data .= substr($this->_long2str($w[1]), 0, $len % 4); 267 | } 268 | } else { 269 | $data .= substr($this->_long2str($w[0]), 0, $len % 4); 270 | } 271 | } 272 | } 273 | return $data; 274 | } 275 | // }}} 276 | // {{{ _encipherLong() 277 | /** 278 | * Encipher a single long (32-bit) value. 279 | * 280 | * @param integer $y 32 bits of data. 281 | * @param integer $z 32 bits of data. 282 | * @param array &$w Placeholder for enciphered 64 bits (in w[0] and w[1]). 283 | * @param array &$k Key 128 bits (in k[0]-k[3]). 284 | * 285 | * @access private 286 | * @author Jeroen Derks 287 | * @see $n_iter, _add(), _rshift(), _decipherLong() 288 | */ 289 | function _encipherLong($y, $z, &$w, &$k) 290 | { 291 | $sum = (integer)0; 292 | $delta = 0x9E3779B9; 293 | $n = (integer)$this->n_iter; 294 | while ($n-- > 0) { 295 | $y = $this->_add( 296 | $y, 297 | $this->_add($z << 4 ^ $this->_rshift($z, 5), $z) ^ 298 | $this->_add($sum, $k[$sum & 3]) 299 | ); 300 | $sum = $this->_add($sum, $delta); 301 | $z = $this->_add( 302 | $z, 303 | $this->_add($y << 4 ^ $this->_rshift($y, 5), $y) ^ 304 | $this->_add($sum, $k[$this->_rshift($sum, 11) & 3]) 305 | ); 306 | } 307 | $w[0] = $y; 308 | $w[1] = $z; 309 | } 310 | // }}} 311 | // {{{ _decipherLong() 312 | /** 313 | * Decipher a single long (32-bit) value. 314 | * 315 | * @param integer $y 32 bits of enciphered data. 316 | * @param integer $z 32 bits of enciphered data. 317 | * @param array &$w Placeholder for deciphered 64 bits (in w[0] and w[1]). 318 | * @param array &$k Key 128 bits (in k[0]-k[3]). 319 | * 320 | * @access private 321 | * @author Jeroen Derks 322 | * @see $n_iter, _add(), _rshift(), _decipherLong() 323 | */ 324 | function _decipherLong($y, $z, &$w, &$k) 325 | { 326 | // sum = delta<<5, in general sum = delta * n 327 | $sum = 0xC6EF3720; 328 | $delta = 0x9E3779B9; 329 | $n = (integer)$this->n_iter; 330 | while ($n-- > 0) { 331 | $z = $this->_add( 332 | $z, 333 | -($this->_add($y << 4 ^ $this->_rshift($y, 5), $y) ^ 334 | $this->_add($sum, $k[$this->_rshift($sum, 11) & 3])) 335 | ); 336 | $sum = $this->_add($sum, -$delta); 337 | $y = $this->_add( 338 | $y, 339 | -($this->_add($z << 4 ^ $this->_rshift($z, 5), $z) ^ 340 | $this->_add($sum, $k[$sum & 3])) 341 | ); 342 | } 343 | $w[0] = $y; 344 | $w[1] = $z; 345 | } 346 | // }}} 347 | // {{{ _resize() 348 | /** 349 | * Resize data string to a multiple of specified size. 350 | * 351 | * @param string $data Data string to resize to specified size. 352 | * @param integer $size Size in bytes to align data to. 353 | * @param boolean $nonull Set to true if padded bytes should not be zero. 354 | * 355 | * @return integer Length of supplied data string. 356 | * 357 | * @access private 358 | * @author Jeroen Derks 359 | */ 360 | function _resize(&$data, $size, $nonull = false) 361 | { 362 | 363 | $n = strlen($data); 364 | $nmod = $n % $size; 365 | if ($nmod > 0) { 366 | if ($nonull) { 367 | for ($i = $n; $i < $n - $nmod + $size; ++$i) { 368 | $data[$i] = $data[$i % $n]; 369 | } 370 | } else { 371 | for ($i = $n; $i < $n - $nmod + $size; ++$i) { 372 | $data[$i] = chr(0); 373 | } 374 | } 375 | } 376 | return $n; 377 | } 378 | // }}} 379 | // {{{ _hex2bin() 380 | /** 381 | * Convert a hexadecimal string to a binary string (e.g. convert "616263" to "abc"). 382 | * 383 | * @param string $str Hexadecimal string to convert to binary string. 384 | * 385 | * @return string Binary string. 386 | * 387 | * @access private 388 | * @author Jeroen Derks 389 | */ 390 | function _hex2bin($str) 391 | { 392 | $len = strlen($str); 393 | return pack('H' . $len, $str); 394 | } 395 | // }}} 396 | // {{{ _str2long() 397 | /** 398 | * Convert string to array of long. 399 | * 400 | * @param integer $start Index into $data_long for output. 401 | * @param string &$data Input string. 402 | * @param array &$data_long Output array of long. 403 | * 404 | * @return integer Index from which to optionally continue. 405 | * 406 | * @access private 407 | * @author Jeroen Derks 408 | */ 409 | function _str2long($start, &$data, &$data_long) 410 | { 411 | $n = strlen($data); 412 | $tmp = unpack('N*', $data); 413 | $j = $start; 414 | foreach ($tmp as $value) 415 | $data_long[$j++] = $value; 416 | return $j; 417 | } 418 | // }}} 419 | // {{{ _long2str() 420 | /** 421 | * Convert long to character string. 422 | * 423 | * @param long $l Long to convert to character string. 424 | * 425 | * @return string Character string. 426 | * 427 | * @access private 428 | * @author Jeroen Derks 429 | */ 430 | function _long2str($l) 431 | { 432 | return pack('N', $l); 433 | } 434 | // }}} 435 | // {{{ _rshift() 436 | 437 | /** 438 | * Handle proper unsigned right shift, dealing with PHP's signed shift. 439 | * 440 | * @access private 441 | * @since 2004/Sep/06 442 | * @author Jeroen Derks 443 | */ 444 | function _rshift($integer, $n) 445 | { 446 | // convert to 32 bits 447 | if (0xffffffff < $integer || -0xffffffff > $integer) { 448 | $integer = fmod($integer, 0xffffffff + 1); 449 | } 450 | // convert to unsigned integer 451 | if (0x7fffffff < $integer) { 452 | $integer -= 0xffffffff + 1.0; 453 | } elseif (-0x80000000 > $integer) { 454 | $integer += 0xffffffff + 1.0; 455 | } 456 | // do right shift 457 | if (0 > $integer) { 458 | $integer &= 0x7fffffff; // remove sign bit before shift 459 | $integer >>= $n; // right shift 460 | $integer |= 1 << (31 - $n); // set shifted sign bit 461 | } else { 462 | $integer >>= $n; // use normal right shift 463 | } 464 | return $integer; 465 | } 466 | // }}} 467 | // {{{ _add() 468 | 469 | /** 470 | * Handle proper unsigned add, dealing with PHP's signed add. 471 | * 472 | * @access private 473 | * @since 2004/Sep/06 474 | * @author Jeroen Derks 475 | */ 476 | function _add($i1, $i2) 477 | { 478 | $result = 0.0; 479 | foreach (func_get_args() as $value) { 480 | // remove sign if necessary 481 | if (0.0 > $value) { 482 | $value -= 1.0 + 0xffffffff; 483 | } 484 | $result += $value; 485 | } 486 | // convert to 32 bits 487 | if (0xffffffff < $result || -0xffffffff > $result) { 488 | $result = fmod($result, 0xffffffff + 1); 489 | } 490 | // convert to signed integer 491 | if (0x7fffffff < $result) { 492 | $result -= 0xffffffff + 1.0; 493 | } elseif (-0x80000000 > $result) { 494 | $result += 0xffffffff + 1.0; 495 | } 496 | return $result; 497 | } 498 | // }}} 499 | } 500 | -------------------------------------------------------------------------------- /research/xtea3.php: -------------------------------------------------------------------------------- 1 | 16) { 31 | throw new \InvalidArgumentException("the max length for a XTEA binary key is 16 bytes."); 32 | } elseif ($padding_scheme === self::PAD_NONE && $len !== 16) { 33 | throw new \InvalidArgumentException("with PAD_NONE the key has to be _EXACTLY_ 16 bytes long."); 34 | } elseif ($len < 16) { 35 | $key .= str_repeat("\x00", 16 - $len); 36 | } else { 37 | // all good 38 | } 39 | $ret = []; 40 | foreach (str_split($key, 4) as $key) { 41 | $ret[] = self::from_little_uint32_t($key); 42 | } 43 | assert(count($ret) === 4); 44 | return $ret; 45 | } 46 | /** 47 | * xtea-encrypt data 48 | * 49 | * @param string $data 50 | * @param int[4] $keys 51 | * @param integer $padding_scheme 52 | * @param integer $rounds 53 | * @return string 54 | */ 55 | public static function encrypt(string $data, array $keys, int $padding_scheme = self::PAD_NONE, int $rounds = 32) : string 56 | { 57 | if ($padding_scheme < 0 || $padding_scheme > 2) { 58 | throw new \InvalidArgumentException("only PAD_NONE and PAD_0x00 and PAD_RANDOM supported!"); 59 | } 60 | if (count($keys) !== 4) { 61 | throw new \InvalidArgumentException('count($keys) !== 4'); 62 | } 63 | for ($i = 0; $i < 4; ++$i) { 64 | if (!is_int($keys[$i])) { 65 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 66 | } 67 | if ($keys[$i] < 0) { 68 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 69 | } 70 | if ($keys[$i] > 0xFFFFFFFF) { 71 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 72 | } 73 | } 74 | if ($rounds < 0) { 75 | throw new \InvalidArgumentException(" < 0 rounds is impossible (and <32 is probably a bad idea)"); 76 | } 77 | $len = strlen($data); 78 | if ($len === 0 || (($len % 8) !== 0)) { 79 | if ($padding_scheme === self::PAD_NONE) { 80 | throw new \InvalidArgumentException("with PAD_NONE the data MUST be a multiple of 8 bytes!"); 81 | } else { 82 | // encrypt_unsafe will take care of it. 83 | } 84 | } 85 | // we have now verified that everything is safe. 86 | return self::encrypt_unsafe($data, $keys, $padding_scheme, $rounds); 87 | } 88 | /** 89 | * faster version of encrypt(), lacking input validation. 90 | * 91 | * @param string $data 92 | * @param int[4] $keys 93 | * @param integer $padding_scheme 94 | * @param integer $rounds 95 | * @return string 96 | */ 97 | public static function encrypt_unsafe(string $data, array $keys, int $padding_scheme = self::PAD_NONE, int $rounds = 32) : string 98 | { 99 | $len = strlen($data); 100 | if ($len === 0) { 101 | $len = 8; 102 | if ($padding_scheme === self::PAD_0x00) { 103 | $data = str_repeat("\x00", 8); 104 | } else { 105 | // self::PAD_RANDOM 106 | $data = random_bytes(8); 107 | } 108 | } elseif ((($len % 8) !== 0)) { 109 | $nearest = (int)(ceil($len / 8) * 8); 110 | assert($nearest !== $len); 111 | assert($nearest > $len); 112 | if ($padding_scheme === self::PAD_0x00) { 113 | $data .= str_repeat("\x00", $nearest - $len); 114 | } else { 115 | // self::PAD_RANDOM 116 | $data .= random_bytes($nearest - $len); 117 | } 118 | $len = $nearest; 119 | } 120 | // good to go 121 | $ret = ''; 122 | for ($i = 0; $i < $len; $i += 8) { 123 | $i1 = self::from_little_uint32_t(substr($data, $i, 4)); 124 | $i2 = self::from_little_uint32_t(substr($data, $i + 4, 4)); 125 | self::encipher_unsafe($i1, $i2, $keys, $rounds); 126 | $ret .= self::to_little_uint32_t($i1); 127 | $ret .= self::to_little_uint32_t($i2); 128 | } 129 | return $ret; 130 | } 131 | /** 132 | * xtea-decrypt data 133 | * 134 | * @param string $data 135 | * @param int[4] $keys 136 | * @param integer $rounds 137 | * @return string decrypted 138 | */ 139 | public static function decrypt(string $data, array $keys, int $rounds = 32) : string 140 | { 141 | $len = strlen($data); 142 | if ($len < 8) { 143 | throw new \InvalidArgumentException("this cannot be (intact) xtea-encrypted data, it's less than 8 bytes long (the minimum xtea length)"); 144 | } 145 | if (($len % 8) !== 0) { 146 | throw new \InvalidArgumentException("this cannot be (intact) xtea-encrypted data, the length is not a multiple of 8 bytes."); 147 | } 148 | if (count($keys) !== 4) { 149 | throw new \InvalidArgumentException('count($keys) !== 4'); 150 | } 151 | for ($i = 0; $i < 4; ++$i) { 152 | if (!is_int($keys[$i])) { 153 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 154 | } 155 | if ($keys[$i] < 0) { 156 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 157 | } 158 | if ($keys[$i] > 0xFFFFFFFF) { 159 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 160 | } 161 | } 162 | if ($rounds < 0) { 163 | throw new \InvalidArgumentException(" < 0 rounds is impossible (and <32 is probably a bad idea)"); 164 | } 165 | return self::decrypt_unsafe($data, $keys, $rounds); 166 | } 167 | /** 168 | * faster version of decrypt() but lacking input validation. 169 | * 170 | * @param string $data 171 | * @param int[4] $keys 172 | * @param integer $rounds 173 | * @return string decrypted 174 | */ 175 | public static function decrypt_unsafe(string $data, array $keys, int $rounds = 32) : string 176 | { 177 | // good to go 178 | $ret = ''; 179 | $len = strlen($data); 180 | for ($i = 0; $i < $len; $i += 8) { 181 | $i1 = self::from_little_uint32_t(substr($data, $i, 4)); 182 | $i2 = self::from_little_uint32_t(substr($data, $i + 4, 4)); 183 | self::decipher_unsafe($i1, $i2, $keys, $rounds); 184 | $ret .= self::to_little_uint32_t($i1); 185 | $ret .= self::to_little_uint32_t($i2); 186 | } 187 | return $ret; 188 | 189 | } 190 | 191 | //////////// internal functions /////////////////// 192 | protected static function from_little_uint32_t(string $i) : int 193 | { 194 | $arr = unpack('Vuint32_t', $i); 195 | return $arr['uint32_t']; 196 | } 197 | protected static function to_little_uint32_t(int $i) : string 198 | { 199 | return pack('V', $i); 200 | } 201 | protected static function encipher(int &$data1, int &$data2, array $keys, int $rounds) 202 | { 203 | { 204 | // 205 | if ($data1 < 0) { 206 | throw new \InvalidArgumentException('$data1 < 0'); 207 | } 208 | if ($data2 < 0) { 209 | throw new \InvalidArgumentException('$data2 < 0'); 210 | } 211 | if ($data1 > 0xFFFFFFFF) { 212 | throw new \InvalidArgumentException('$data1 > 0xFFFFFFFF'); 213 | } 214 | if ($data2 > 0xFFFFFFFF) { 215 | throw new \InvalidArgumentException('$data2 > 0xFFFFFFFF'); 216 | } 217 | 218 | if (count($keys) !== 4) { 219 | throw new \InvalidArgumentException('count($keys) !== 4'); 220 | } 221 | for ($i = 0; $i < 4; ++$i) { 222 | if (!is_int($keys[$i])) { 223 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 224 | } 225 | if ($keys[$i] < 0) { 226 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 227 | } 228 | if ($keys[$i] > 0xFFFFFFFF) { 229 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 230 | } 231 | } 232 | // 233 | } 234 | self::encipher_unsafe($data1, $data2, $keys, $rounds); 235 | return; // void 236 | } 237 | protected static function encipher_unsafe(int &$data1, int &$data2, array $keys, int $rounds) : void 238 | { 239 | $sum = 0; 240 | for ($i = 0; $i < $rounds; ++$i) { 241 | $data1 = self::_add( 242 | $data1, 243 | self::_add($data2 << 4 ^ self::_rshift($data2, 5), $data2) ^ 244 | self::_add($sum, $keys[$sum & 3]) 245 | ); 246 | $sum = self::_add($sum, 0x9e3779b9); // delta 247 | $data2 = self::_add( 248 | $data2, 249 | self::_add($data1 << 4 ^ self::_rshift($data1, 5), $data1) ^ 250 | self::_add($sum, $keys[self::_rshift($sum, 11) & 3]) 251 | ); 252 | } 253 | $data1 = (int)$data1; 254 | $data2 = (int)$data2; 255 | } 256 | protected static function decipher_unsafe(int &$data1, int &$data2, array $keys, int $rounds) 257 | { 258 | $sum = self::_add(0, 0x9E3779B9 * $rounds); // 0x9E3779B9 = delta 259 | for ($i = 0; $i < $rounds; ++$i) { 260 | $data2 = self::_add( 261 | $data2, 262 | -(self::_add($data1 << 4 ^ self::_rshift($data1, 5), $data1) ^ 263 | self::_add($sum, $keys[self::_rshift($sum, 11) & 3])) 264 | ); 265 | $sum = self::_add($sum, -(0x9E3779B9)); // 0x9E3779B9 = delta 266 | $data1 = self::_add( 267 | $data1, 268 | -(self::_add($data2 << 4 ^ self::_rshift($data2, 5), $data2) ^ 269 | self::_add($sum, $keys[$sum & 3])) 270 | ); 271 | } 272 | $data1 = (int)$data1; 273 | $data2 = (int)$data2; 274 | } 275 | /** 276 | * Handle proper unsigned right shift, dealing with PHP's signed shift. 277 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 278 | * @access private 279 | * @since 2004/Sep/06 280 | * @author Jeroen Derks 281 | */ 282 | protected static function _rshift($integer, $n) 283 | { 284 | // convert to 32 bits 285 | if (0xffffffff < $integer || -0xffffffff > $integer) { 286 | $integer = fmod($integer, 0xffffffff + 1); 287 | } 288 | // convert to unsigned integer 289 | if (0x7fffffff < $integer) { 290 | $integer -= 0xffffffff + 1.0; 291 | } elseif (-0x80000000 > $integer) { 292 | $integer += 0xffffffff + 1.0; 293 | } 294 | // do right shift 295 | if (0 > $integer) { 296 | $integer &= 0x7fffffff; // remove sign bit before shift 297 | $integer >>= $n; // right shift 298 | $integer |= 1 << (31 - $n); // set shifted sign bit 299 | } else { 300 | $integer >>= $n; // use normal right shift 301 | } 302 | return $integer; 303 | } 304 | 305 | /** 306 | * Handle proper unsigned add, dealing with PHP's signed add. 307 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 308 | * @access private 309 | * @since 2004/Sep/06 310 | * @author Jeroen Derks 311 | */ 312 | function _add($i1, $i2) 313 | { 314 | $result = 0.0; 315 | foreach ([$i1, $i2] as $value) { 316 | // remove sign if necessary 317 | if (0.0 > $value) { 318 | $value -= 1.0 + 0xffffffff; 319 | } 320 | $result += $value; 321 | } 322 | // convert to 32 bits 323 | if (0xffffffff < $result || -0xffffffff > $result) { 324 | $result = fmod($result, 0xffffffff + 1); 325 | } 326 | // convert to signed integer 327 | if (0x7fffffff < $result) { 328 | $result -= 0xffffffff + 1.0; 329 | } elseif (-0x80000000 > $result) { 330 | $result += 0xffffffff + 1.0; 331 | } 332 | return $result; 333 | } 334 | } 335 | 336 | 337 | 338 | $keys_binary = random_bytes(4 * 4); 339 | $keys_array = []; 340 | 341 | foreach (str_split($keys_binary, 4) as $tmp) { 342 | $keys_array[] = from_little_uint32_t($tmp); 343 | } 344 | unset($tmp); 345 | 346 | 347 | $correct = new Xtea_helper($keys_binary); 348 | $data = "hello world!1234"; 349 | $mul = 1030; 350 | $data = random_bytes($mul * 8); 351 | if(strlen($data)!==($mul*8)){ 352 | die("WTF!!!"); 353 | } 354 | $encrypted_cpp = $correct->encrypt($data); 355 | $encrypted_php = Xtea::encrypt($data, $keys_array); 356 | $decrypted_cpp = $correct->decrypt($encrypted_cpp); 357 | $decrypted_php = Xtea::decrypt($encrypted_php, $keys_array); 358 | 359 | hhb_var_dump( 360 | $encrypted_php === $encrypted_cpp, 361 | $decrypted_cpp === $decrypted_php, 362 | $decrypted_cpp === $data, 363 | $decrypted_php === $data 364 | //,$data 365 | ); 366 | $data = bin2hex($data); 367 | 368 | function encrypt(string $data, array $keys) 369 | { 370 | if ((strlen($data) % 8) !== 0) { 371 | throw new \InvalidArgumentException(); 372 | } 373 | if (count($keys) !== 4) { 374 | throw new \InvalidArgumentException(); 375 | } 376 | $ret = ''; 377 | foreach (str_split($data, 8) as $data) { 378 | $chunks = str_split($data, 4); 379 | $i1 = from_little_uint32_t($chunks[0]); 380 | $i2 = from_little_uint32_t($chunks[1]); 381 | encipher($i1, $i2, $keys); 382 | $ret .= to_little_uint32_t($i1); 383 | $ret .= to_little_uint32_t($i2); 384 | } 385 | return $ret; 386 | } 387 | 388 | 389 | function encipher(int &$data1, int &$data2, array $keys) 390 | { 391 | { 392 | // 393 | if ($data1 < 0) { 394 | throw new \InvalidArgumentException('$data1 < 0'); 395 | } 396 | if ($data2 < 0) { 397 | throw new \InvalidArgumentException('$data2 < 0'); 398 | } 399 | if ($data1 > 0xFFFFFFFF) { 400 | throw new \InvalidArgumentException('$data1 > 0xFFFFFFFF'); 401 | } 402 | if ($data2 > 0xFFFFFFFF) { 403 | throw new \InvalidArgumentException('$data2 > 0xFFFFFFFF'); 404 | } 405 | 406 | if (count($keys) !== 4) { 407 | throw new \InvalidArgumentException('count($keys) !== 4'); 408 | } 409 | for ($i = 0; $i < 4; ++$i) { 410 | if (!is_int($keys[$i])) { 411 | throw new \InvalidArgumentException('!is_int($keys[' . $i . '])'); 412 | } 413 | if ($keys[$i] < 0) { 414 | throw new \InvalidArgumentException('$keys[' . $i . '] < 0'); 415 | } 416 | if ($keys[$i] > 0xFFFFFFFF) { 417 | throw new \InvalidArgumentException('$keys[' . $i . '] > 0xFFFFFFFF'); 418 | } 419 | } 420 | // 421 | } 422 | encipher_unsafe3($data1, $data2, $keys); 423 | } 424 | 425 | function encipher_unsafe3(int &$data1, int &$data2, array $keys, $rounds = 32) 426 | { 427 | $y = $data1; 428 | $z = $data2; 429 | $sum = 0; 430 | $delta = 0x9e3779b9; 431 | /* start cycle */ 432 | for ($i = 0; $i < $rounds; ++$i) { 433 | $y = _add( 434 | $y, 435 | _add($z << 4 ^ _rshift($z, 5), $z) ^ 436 | _add($sum, $keys[$sum & 3]) 437 | ); 438 | $sum = _add($sum, $delta); 439 | $z = _add( 440 | $z, 441 | _add($y << 4 ^ _rshift($y, 5), $y) ^ 442 | _add($sum, $keys[_rshift($sum, 11) & 3]) 443 | ); 444 | } 445 | /* end cycle */ 446 | $v[0] = $y; 447 | $v[1] = $z; 448 | //return array($y, $z); 449 | $data1 = (int)$y; 450 | $data2 = (int)$z; 451 | } 452 | 453 | function encipher_unsafe(int &$data1, int &$data2, array $key) 454 | { 455 | $v0 = $data1; 456 | $v1 = $data2; 457 | $sum = 0; 458 | $delta = 0x9E3779B9; 459 | for ($i = 0; $i < 32; ++$i) { 460 | $v0 += ((($v1 << 4) ^ ($v1 >> 5)) + $v1) ^ ($sum + $key[$sum & 3]); 461 | $sum += $delta; 462 | $v1 += ((($v0 << 4) ^ ($v0 >> 5)) + $v0) ^ ($sum + $key[($sum >> 11) & 3]); 463 | } 464 | $data1 = $v0; 465 | $data2 = $v1; 466 | } 467 | function _decipherLong($y, $z, &$w, &$k) 468 | { 469 | // sum = delta<<5, in general sum = delta * n 470 | $sum = 0xC6EF3720; 471 | $delta = 0x9E3779B9; 472 | $n = (integer)$this->n_iter; 473 | while ($n-- > 0) { 474 | $z = $this->_add( 475 | $z, 476 | -($this->_add($y << 4 ^ $this->_rshift($y, 5), $y) ^ 477 | $this->_add($sum, $k[$this->_rshift($sum, 11) & 3])) 478 | ); 479 | $sum = $this->_add($sum, -$delta); 480 | $y = $this->_add( 481 | $y, 482 | -($this->_add($z << 4 ^ $this->_rshift($z, 5), $z) ^ 483 | $this->_add($sum, $k[$sum & 3])) 484 | ); 485 | } 486 | $w[0] = $y; 487 | $w[1] = $z; 488 | } 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | /** 499 | * Handle proper unsigned right shift, dealing with PHP's signed shift. 500 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 501 | * @access private 502 | * @since 2004/Sep/06 503 | * @author Jeroen Derks 504 | */ 505 | function _rshift($integer, $n) 506 | { 507 | // convert to 32 bits 508 | if (0xffffffff < $integer || -0xffffffff > $integer) { 509 | $integer = fmod($integer, 0xffffffff + 1); 510 | } 511 | // convert to unsigned integer 512 | if (0x7fffffff < $integer) { 513 | $integer -= 0xffffffff + 1.0; 514 | } elseif (-0x80000000 > $integer) { 515 | $integer += 0xffffffff + 1.0; 516 | } 517 | // do right shift 518 | if (0 > $integer) { 519 | $integer &= 0x7fffffff; // remove sign bit before shift 520 | $integer >>= $n; // right shift 521 | $integer |= 1 << (31 - $n); // set shifted sign bit 522 | } else { 523 | $integer >>= $n; // use normal right shift 524 | } 525 | return $integer; 526 | } 527 | 528 | function _add($v1, $v2) 529 | { 530 | return call_user_func_array('add', func_get_args()); 531 | } 532 | /** 533 | * Handle proper unsigned add, dealing with PHP's signed add. 534 | * taken from https://github.com/pear/Crypt_Xtea/blob/trunk/Xtea.php 535 | * @access private 536 | * @since 2004/Sep/06 537 | * @author Jeroen Derks 538 | */ 539 | function add($i1, $i2) 540 | { 541 | $result = 0.0; 542 | foreach ([$i1, $i2] as $value) { 543 | // remove sign if necessary 544 | if (0.0 > $value) { 545 | $value -= 1.0 + 0xffffffff; 546 | } 547 | $result += $value; 548 | } 549 | // convert to 32 bits 550 | if (0xffffffff < $result || -0xffffffff > $result) { 551 | $result = fmod($result, 0xffffffff + 1); 552 | } 553 | // convert to signed integer 554 | if (0x7fffffff < $result) { 555 | $result -= 0xffffffff + 1.0; 556 | } elseif (-0x80000000 > $result) { 557 | $result += 0xffffffff + 1.0; 558 | } 559 | return $result; 560 | } 561 | 562 | die(); 563 | __halt_compiler (); 564 | /* take 64 bits of data in data_in_out[0] and data_in_out[1] and 128 bits of key[0] - key[3] */ 565 | void encipher(uint32_t & data1, uint32_t & data2, uint32_t const key [4]) { 566 | uint32_t v0 = data1, v1 = data2, sum = 0, delta = 0x9E3779B9; 567 | for (int i = 0; i < 32; ++i) { 568 | v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); 569 | sum += delta; 570 | v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); 571 | } 572 | data1 = v0; 573 | data2 = v1; 574 | } 575 | 576 | void decipher(uint32_t & data1, uint32_t & data2, uint32_t const key [4]) { 577 | const int num_rounds = 32; 578 | const uint32_t delta = 0x9E3779B9; 579 | uint32_t v0 = data1, v1 = data1, sum = delta * num_rounds; 580 | for (int i = 0; i < 32; i ++) { 581 | v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); 582 | sum -= delta; 583 | v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); 584 | } 585 | data1 = v0; 586 | data2 = v1; 587 | } 588 | 589 | void decipher(uint32_t v[2], uint32_t const key [4]) { 590 | unsigned int i; 591 | uint32_t v0 = v[0], v1 = v[1], delta = 0x9E3779B9, sum = delta * num_rounds; 592 | for (i = 0; i < 32; i ++) { 593 | v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); 594 | sum -= delta; 595 | v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); 596 | } 597 | v[0] = v0; 598 | v[1] = v1; 599 | } 600 | -------------------------------------------------------------------------------- /research/xtea_helper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include // std::setw 7 | #include 8 | #include 9 | using namespace std; 10 | 11 | #if !defined(HETOBE16) 12 | #if !defined(__BYTE_ORDER) 13 | #error Failed to detect byte order! 14 | #endif 15 | #if __BYTE_ORDER == __BIG_ENDIAN 16 | #define HETOBE64(x) (x) 17 | #define HETOLE64(x) __bswap_constant_64(x) 18 | #define HETOBE32(x) (x) 19 | #define HETOLE32(x) __bswap_constant_32(x) 20 | #define HETOBE16(x) (x) 21 | #define HETOLE16(x) __bswap_constant_16(x) 22 | // 23 | #define LETOHE64(x) __bswap_constant_64(x) 24 | #define BETOHE64(x) (x) 25 | #define LETOHE32(x) __bswap_constant_32(x) 26 | #define BETOHE32(x) (x) 27 | #define LETOHE16(x) __bswap_constant_16(x) 28 | #define BETOHE16(x) (x) 29 | #else 30 | #if __BYTE_ORDER == __LITTLE_ENDIAN 31 | #define HETOBE64(x) __bswap_constant_64(x) 32 | #define HETOLE64(x) (x) 33 | #define HETOBE32(x) __bswap_constant_32(x) 34 | #define HETOLE32(x) (x) 35 | #define HETOBE16(x) __bswap_constant_16(x) 36 | #define HETOLE16(x) (x) 37 | // 38 | #define LETOHE64(x) (x) 39 | #define BETOHE64(x) __bswap_constant_64(x) 40 | #define LETOHE32(x) (x) 41 | #define BETOHE32(x) __bswap_constant_32(x) 42 | #define LETOHE16(x) (x) 43 | #define BETOHE16(x) __bswap_constant_16(x) 44 | #else 45 | #error Failed to detect byte order! appears to be neither big endian nor little endian.. 46 | #endif 47 | #endif 48 | #endif 49 | 50 | static std::string dump_string(const std::string &input) 51 | { 52 | std::ostringstream escaped; 53 | escaped.fill('0'); 54 | escaped << std::hex; 55 | 56 | for (std::string::const_iterator i = input.begin(), n = input.end(); i != n; ++i) 57 | { 58 | std::string::value_type c = (*i); 59 | 60 | // Keep alphanumeric and other accepted characters intact 61 | if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == ',' || c == '~' || c == ' ' || c == ' ' || c == '!' || c == ':' || c == ';') 62 | { 63 | escaped << c; 64 | continue; 65 | } 66 | // Any other characters are percent-encoded 67 | escaped << std::uppercase; 68 | escaped << '%' << std::setw(2) << int((unsigned char)c); 69 | escaped << std::nouppercase; 70 | } 71 | return ("string(" + std::to_string(input.length()) + "): \"" + escaped.str() + "\""); 72 | } 73 | 74 | static void sendMessage(const string &message) 75 | { 76 | uint16_t size = message.size(); 77 | size = HETOLE16(size); 78 | cout.write((char *)&size, sizeof(size)); 79 | cout << message << flush; 80 | } 81 | 82 | static string readMessage(void) 83 | { 84 | if (!cin.good()) 85 | { 86 | exit(EXIT_FAILURE); 87 | } 88 | uint16_t size; 89 | cin.read((char *)&size, sizeof(size)); 90 | if (!cin.good()) 91 | { 92 | exit(EXIT_FAILURE); 93 | } 94 | size = LETOHE16(size); 95 | if (!size) 96 | { 97 | return ""; 98 | } 99 | string ret(size, '\0'); 100 | cin.read(&ret[0], size); 101 | if (!cin.good()) 102 | { 103 | exit(EXIT_FAILURE); 104 | } 105 | return ret; 106 | } 107 | 108 | /* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */ 109 | 110 | static void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) 111 | { 112 | unsigned int i; 113 | uint32_t v0 = v[0], v1 = v[1], sum = 0, delta = 0x9E3779B9; 114 | for (i = 0; i < num_rounds; i++) 115 | { 116 | v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); 117 | sum += delta; 118 | v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); 119 | } 120 | v[0] = v0; 121 | v[1] = v1; 122 | } 123 | 124 | static void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) 125 | { 126 | unsigned int i; 127 | uint32_t v0 = v[0], v1 = v[1], delta = 0x9E3779B9, sum = delta * num_rounds; 128 | for (i = 0; i < num_rounds; i++) 129 | { 130 | v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); 131 | sum -= delta; 132 | v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); 133 | } 134 | v[0] = v0; 135 | v[1] = v1; 136 | } 137 | int main(int argc, char *argv[]) 138 | { 139 | uint32_t keys[4]; 140 | // init 141 | { 142 | setvbuf(stdin, NULL, _IONBF, 0); 143 | setvbuf(stdout, NULL, _IONBF, 0); 144 | string firstMessage = readMessage(); 145 | if (firstMessage.length() != (4 * 4)) 146 | { 147 | cerr << "ERROR: FIRST MESSAGE WAS NOT EXACTLY 4*4 bytes! (the XTEA keys) - was: " << dump_string(firstMessage) << endl; 148 | exit(EXIT_FAILURE); 149 | } 150 | for (size_t i = 0; i < 4; ++i) 151 | { 152 | keys[i] = LETOHE32(*((uint32_t *)(&firstMessage[i * 4]))); 153 | //cerr << "keys[" << i << "]: " << keys[i] << endl; 154 | } 155 | } 156 | //std::this_thread::sleep_for(std::chrono::seconds(9)); 157 | for (;;) 158 | { 159 | string message = readMessage(); 160 | uint8_t header = uint8_t(message[0]); 161 | if (header == 0) 162 | { 163 | // encrypt 164 | message.erase(0, 3); // u8 message header and u16 length header.. 165 | //cerr << "TO ENCRYPT: " << dump_string(message) << endl; 166 | for (size_t i = 0; i < message.length(); i += 8) 167 | { 168 | encipher(32, (uint32_t *)&message[i], keys); 169 | } 170 | //cerr << "ENCRYPTED: " << dump_string(message) << endl; 171 | sendMessage(message); 172 | } 173 | else if (header == 1) 174 | { 175 | // decrypt 176 | message.erase(0, 3); // u8 message header and u16 length header.. 177 | //cerr << "TO DECRYPT: " << dump_string(message) << endl; 178 | for (size_t i = 0; i < message.length(); i += 8) 179 | { 180 | decipher(32, (uint32_t *)&message[i], keys); 181 | } 182 | //cerr << "DECRYPTED: " << dump_string(message) << endl; 183 | sendMessage(message); 184 | } 185 | else 186 | { 187 | std::runtime_error("invalid message header! " + dump_string(message)); 188 | } 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /research/xtea_helper.php: -------------------------------------------------------------------------------- 1 | keys_binary = $keys_binary; 18 | $descriptorspec = array( 19 | 0 => array("pipe", "rb"), // stdin 20 | 1 => array("pipe", "wb"), // stdout 21 | // stderr: default behaviour: inherit and share the parent (our) stderr. 22 | ); 23 | $cmd; 24 | if (false !== stripos(PHP_OS, 'cygwin')) { 25 | $cmd = './xtea_helper.exe'; 26 | } elseif (false !== stripos(PHP_OS, 'windows')) { 27 | $cmd = 'xtea_helper.exe'; 28 | } else { 29 | $cmd = './xtea_helper'; 30 | } 31 | $this->helper_proc = proc_open($cmd, $descriptorspec, $this->pipes); 32 | if (false == $this->helper_proc) { 33 | throw new \RuntimeException("failed to start extea_helper! cmd: {$cmd}"); 34 | } 35 | stream_set_blocking($this->pipes[0], true); 36 | stream_set_blocking($this->pipes[1], true); 37 | $this->sendMessage($keys_binary); 38 | } 39 | function __destruct() 40 | { 41 | fclose($this->pipes[0]); 42 | fclose($this->pipes[1]); 43 | proc_terminate($this->helper_proc); 44 | proc_close($this->helper_proc); 45 | } 46 | public function encrypt(string $data) : string 47 | { 48 | if ((($len = strlen($data)) % 8) !== 0) { 49 | // needs length padding 50 | $nearest = (int)(ceil($len / 8) * 8); 51 | $data .= str_repeat("p", $nearest - $len); 52 | } elseif ($len === 0) { 53 | $data = str_repeat("p", 8); 54 | } 55 | $message = chr($this::ENCRYPT_COMMAND) . to_little_uint16_t(strlen($data)) . $data; 56 | $this->sendMessage($message); 57 | $ret = $this->readMessage(); 58 | return $ret; 59 | } 60 | public function decrypt(string $data) : string 61 | { 62 | if ((($len = strlen($data)) % 8) !== 0) { 63 | // needs length padding 64 | $nearest = (int)(ceil($len / 8) * 8); 65 | $data .= str_repeat("\x00", $nearest - $len); 66 | } elseif ($len === 0) { 67 | $data = str_repeat("\x00", 8); 68 | } 69 | $message = chr($this::DECRYPT_COMMAND) . to_little_uint16_t(strlen($data)) . $data; 70 | $this->sendMessage($message); 71 | return $this->readMessage(); 72 | } 73 | protected function sendMessage(string $message) : void 74 | { 75 | fwrite($this->pipes[0], to_little_uint16_t(strlen($message)) . $message); 76 | } 77 | protected function readMessage() 78 | { 79 | $size = fread($this->pipes[1], 2); 80 | $size = from_little_uint16_t($size); 81 | $ret = fread($this->pipes[1], $size); 82 | return $ret; 83 | } 84 | } -------------------------------------------------------------------------------- /tests/lazer.php: -------------------------------------------------------------------------------- 1 | cbc = false; 6 | $original = "\xFF"; 7 | for ($i = 0; $i < 10; ++$i) { 8 | $encrypted = $xtea->Encrypt($original); 9 | $decrypted = $xtea->Decrypt($encrypted); 10 | hhb_var_dump( 11 | $original, 12 | $encrypted, 13 | $decrypted, 14 | bin2hex($original), 15 | bin2hex($decrypted), 16 | ($original === $decrypted), 17 | $xtea->check_implementation() 18 | ); 19 | if (($original === $decrypted)) { 20 | die("SUCCESS!"); 21 | } 22 | $original .= "\xFF"; 23 | } -------------------------------------------------------------------------------- /tests/loginPlayerBot.php: -------------------------------------------------------------------------------- 1 | say("hello from PHP! will read packets."); 18 | for (;; ) { 19 | $xtea_decrypted = null; 20 | $adler_removed = null; 21 | $packet = $tc->internal->read_next_packet(true, true, true, true, $adler_removed, $xtea_decrypted); 22 | $parsed = $tc->internal->parse_packet($packet); 23 | $tc->ping(); 24 | if ($parsed->type === 0x17) { 25 | foreach ($parsed->data['welcome_messages'] as $message) { 26 | echo "server message: {$message}\n"; 27 | } 28 | } 29 | if ($parsed->type === $parsed::TYPE_SAY) { 30 | $name = $parsed->data['speaker_name']; 31 | if ($name === $tc->internal->charname) { 32 | continue; 33 | } 34 | $text = $parsed->data['text']; 35 | echo "{$name}: {$text}\n"; 36 | if ($text === "go up") { 37 | $tc->say("yes sir!"); 38 | $tc->walk_up(); 39 | } elseif ($text === "go down") { 40 | $tc->say("yes sir!"); 41 | $tc->walk_down(); 42 | } elseif ($text === "go right") { 43 | $tc->say("yes sir!"); 44 | $tc->walk_right(); 45 | } elseif ($text === "go left") { 46 | $tc->say("yes sir!"); 47 | $tc->walk_left(); 48 | } elseif ($text === "logout") { 49 | $tc->say("yes, goodbye sir!"); 50 | die(); 51 | } else { 52 | $tc->say("sorry sir, i do not understand the command \"{$text}\""); 53 | } 54 | // var_dump($parsed); 55 | } 56 | continue; 57 | //$packet = bin2hex($packet); 58 | hhb_var_dump( 59 | $adler_removed, 60 | $xtea_decrypted, 61 | bin2hex($packet), 62 | $packet, 63 | $parsed 64 | ); 65 | } 66 | // die(); 67 | //} 68 | 69 | -------------------------------------------------------------------------------- /tests/xtea_tests.php: -------------------------------------------------------------------------------- 1 |