|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App;
|
|
|
|
|
|
|
|
class Utils {
|
|
|
|
// I2P uses custom base64 alphabet, defining translation here
|
|
|
|
private const b64Trans = array("-" => "+", "~" => "/");
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|