├── 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 |