"+", "~" => "/"); // base32 alpabet private const b32alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; /** * base32 encoding function. * @param string $data * @return string */ private static function b32encode(string $data): string { if (empty($data)) { return ""; } /* Create binary string zeropadded to eight bits. */ $data = str_split($data); $binary = implode("", array_map(function ($character) { return sprintf("%08b", ord($character)); }, $data)); /* Split to five bit chunks and make sure last chunk has five bits. */ $binary = str_split($binary, 5); $last = array_pop($binary); if (null !== $last) { $binary[] = str_pad($last, 5, "0", STR_PAD_RIGHT); } /* Convert each five bits to Base32 character. */ $encoded = implode("", array_map(function ($fivebits) { $index = bindec($fivebits); return self::b32alphabet[$index]; }, $binary)); return $encoded; } /** * I2P base64 decoding function. * @param string $data * @return string */ private static function b64decode(string $data): string { return base64_decode(strtr($data, static::b64Trans), true); } /** * Checks if the given domain is in Punycode. * @param string $domain The domain to check. * @return bool Whether the domain is in Punycode. */ public static function isPunycodeDomain($domain): bool { $hasPunycode = false; foreach (explode('.', $domain) as $part) { if (false === static::isAscii($part)) return false; if (static::isPunycode($part)) $hasPunycode = true; } return $hasPunycode; } /** * Checks if the given value is in ASCII character encoding. * @param string $value The value to check. * @return bool Whether the value is in ASCII character encoding. */ public static function isAscii($value): bool { return ('ASCII' === mb_detect_encoding($value, 'ASCII', true)); } /** * Checks if the given value is in Punycode. * @param string $value The value to check. * @return bool Whether the value is in Punycode. * @throws \LogicException If the string is not encoded by UTF-8. */ public static function isPunycode($value): bool { if (false === static::isAscii($value)) return false; if ('UTF-8' !== mb_detect_encoding($value, 'UTF-8', true)) throw new \LogicException('The string should be encoded by UTF-8 to do the right check.'); return (0 === mb_stripos($value, 'xn--', 0, 'UTF-8')); } /** * base64 validation function. * @param string $data * @return bool */ public static function isValidBase64(string $data): bool { $len = strlen($data); if($len < 516 || $len > 616) { return false; } /* .i2p in string */ if(preg_match('/\.i2p/', $data)) { return false; } /* DSA-SHA1. Will reject them because they are old (length 516) */ if($len == 516 && preg_match('/^[a-zA-Z0-9\-~]+AA$/', $data)) { return false; } /* ECDSA or EdDSA */ if($len == 524 && !preg_match('/^[a-zA-Z0-9\-~]+AEAA[Ec]AAA==$/', $data)) { return false; } return true; } /** * I2P base64 to base32 conversion function. * @param string $data * @return string */ public static function b32from64(string $data): string { return self::b32encode(hash('sha256', self::b64decode($data), true)); } /** * Domain validation function. * @param string $data * @return bool */ public static function isValidDomain(string $data, string &$result): bool { $len = strlen($data); if($len < 5 || $len > 255) { // 255 max: rfc2181, section 11 $result = "Domain must be longer than 5 and lesser than 255 chars."; return false; } if(preg_match('/(?:^b32|\.b32)\.i2p$/', $data)) { $result = "Domain can't be b32.i2p or end with .b32.i2p."; return false; } if(preg_match('/(?:^console|\.console)\.i2p$/', $data)) { $result = "Domain can't be console.i2p or end with .console.i2p."; return false; } if(preg_match('/(?:^proxy|\.proxy)\.i2p$/', $data)) { $result = "Domain can't be proxy.i2p or end with .proxy.i2p."; return false; } if(preg_match('/(?:^router|\.router)\.i2p$/', $data)) { $result = "Domain can't be router.i2p or end with .router.i2p."; return false; } if(filter_var($data, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false || !preg_match('/\.i2p$/', $data)) { $result = "Domain is not valid. The label must be less than 63 characters long, contain only alphanumerics or hyphens and end with .i2p."; return false; } return true; } public static function verifyHostRecord(string $data, &$output): bool { $retval = null; if (strpos($data, "&") !== false) return false; // registration string can't have ampersands $cmd = dirname(__FILE__) . '/../bin/verifyhost ' . escapeshellarg($data); exec($cmd, $output, $retval); if (!$retval) return true; return false; } public static function parseHostRecord(string $data): array { $record = []; /* Return empty array if no data provided */ if(!strlen($data)) return []; $data = trim($data); /* Split addressbook host record and extended commands */ $tmp = preg_split("/#!/", $data); /* Parse host record */ $host = preg_split("/\=/", $tmp[0], 2); /* Return empty array if host or b64 is not set */ if(!isset($host[0]) || !isset($host[1])) return []; /* Return if base64 can't be decoded */ if(self::b64decode($host[1]) === false) return []; $record["host"] = $host[0]; $record["b64"] = $host[1]; /* Parse extended commands */ if(isset($tmp[1])) { $cmds = []; $cmdlist = preg_split("/#/", $tmp[1]); foreach($cmdlist as $i) { list($name, $value) = explode('=', $i, 2); $cmds[$name] = $value; } $record["commands"] = $cmds; } return $record; } public static function getResponseHeader($header, $response) { foreach ($response as $key => $r) { // Match the header name up to ':', compare lower case if (stripos($r, $header . ':') === 0) { list($headername, $headervalue) = explode(":", $r, 2); return trim($headervalue); } } } }