openSocket(); try { $this->command(['/login', '=name='.$this->username, '=password='.$this->password]); } catch (RuntimeException $exception) { if (! str_starts_with($exception->getMessage(), 'COMMAND_REJECTED')) { throw $exception; } // RouterOS before 6.43 uses challenge-response authentication. $this->disconnect(); $this->openSocket(); $challenge = $this->command(['/login'])[0]['ret'] ?? null; if (! is_string($challenge) || ! ctype_xdigit($challenge)) { throw new RuntimeException('AUTHENTICATION_FAILED: login RouterOS ditolak.'); } $response = '00'.md5(chr(0).$this->password.pack('H*', $challenge)); $this->command(['/login', '=name='.$this->username, '=response='.$response]); } } private function openSocket(): void { $transport = $this->tls ? 'tls' : 'tcp'; $context = stream_context_create(['ssl' => ['verify_peer' => $this->verifyTls, 'verify_peer_name' => $this->verifyTls, 'SNI_enabled' => true]]); $socket = @stream_socket_client("{$transport}://{$this->host}:{$this->port}", $errorNumber, $errorMessage, $this->timeout, STREAM_CLIENT_CONNECT, $context); if (! is_resource($socket)) { throw new RuntimeException('DEVICE_UNREACHABLE: koneksi RouterOS API gagal.'); } $this->socket = $socket; stream_set_timeout($this->socket, $this->timeout); } public function disconnect(): void { if (is_resource($this->socket)) { fclose($this->socket); } $this->socket = null; } /** @return list> */ public function command(array $words): array { if (! is_resource($this->socket)) { throw new RuntimeException('DRIVER_NOT_CONNECTED'); } foreach ($words as $word) { $this->writeWord($word); } $this->writeWord(''); $rows = []; while (true) { $sentence = $this->readSentence(); $type = array_shift($sentence); if ($type === '!trap' || $type === '!fatal') { throw new RuntimeException('COMMAND_REJECTED: RouterOS menolak operasi.'); } if ($type === '!re') { $rows[] = $this->attributes($sentence); } if ($type === '!empty') { return $rows; } if ($type === '!done') { $attributes = $this->attributes($sentence); if ($attributes !== []) { $rows[] = $attributes; } return $rows; } } } private function writeWord(string $word): void { $length = strlen($word); $prefix = match (true) { $length < 0x80 => chr($length), $length < 0x4000 => pack('n', $length | 0x8000), $length < 0x200000 => substr(pack('N', $length | 0xC0000000), 1), $length < 0x10000000 => pack('N', $length | 0xE0000000), default => chr(0xF0).pack('N', $length), }; $this->writeAll($prefix.$word); } /** @return list */ private function readSentence(): array { $words = []; while (($word = $this->readWord()) !== '') { $words[] = $word; } return $words; } private function readWord(): string { $length = $this->readLength(); return $length === 0 ? '' : $this->readBytes($length); } private function readLength(): int { $first = ord($this->readBytes(1)); if (($first & 0x80) === 0) { return $first; } if (($first & 0xC0) === 0x80) { return (($first & 0x3F) << 8) + ord($this->readBytes(1)); } if (($first & 0xE0) === 0xC0) { $bytes = $this->readBytes(2); return (($first & 0x1F) << 16) + (ord($bytes[0]) << 8) + ord($bytes[1]); } if (($first & 0xF0) === 0xE0) { $bytes = $this->readBytes(3); return (($first & 0x0F) << 24) + (ord($bytes[0]) << 16) + (ord($bytes[1]) << 8) + ord($bytes[2]); } return unpack('N', $this->readBytes(4))[1]; } private function readBytes(int $length): string { $data = ''; while (strlen($data) < $length) { $chunk = fread($this->socket, $length - strlen($data)); if ($chunk === false || $chunk === '') { throw new RuntimeException('CONNECTION_TIMEOUT: respons RouterOS tidak lengkap.'); } $data .= $chunk; } return $data; } private function writeAll(string $data): void { while ($data !== '') { $written = fwrite($this->socket, $data); if ($written === false || $written === 0) { throw new RuntimeException('CONNECTION_FAILED'); } $data = substr($data, $written); } } private function attributes(array $words): array { $attributes = []; foreach ($words as $word) { if (str_starts_with($word, '=')) { [, $key, $value] = array_pad(explode('=', $word, 3), 3, ''); $attributes[$key] = $value; } } return $attributes; } }