diff --git a/README.md b/README.md index 3f10281..34b9e0c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Network Library for PHP with native Yggdrasil support #### Check socket is open -``` +``` php var_dump( \Yggverse\Net\Socket::isOpen('yo.index', 80) ); @@ -20,7 +20,7 @@ var_dump( #### Check host valid -``` +``` php var_dump( \Yggverse\Net\Socket::isHost('yo.index') ); @@ -28,7 +28,7 @@ var_dump( #### Check port valid -``` +``` php var_dump( \Yggverse\Net\Socket::isPort(80) ); @@ -38,15 +38,15 @@ var_dump( #### Resolve records -``` +``` php var_dump( - \Yggverse\Net\Dig::records('yo.index', ['A', 'AAAA']) + \Yggverse\Net\Dig::records('yo.index', ['A', 'AAAA'], &$result = [], &$error = [], $provider = null, $timeout = 5) ); ``` #### Check hostname valid -``` +``` php var_dump( \Yggverse\Net\Dig::isHostName('yo.index') ); @@ -54,7 +54,7 @@ var_dump( #### Check record valid -``` +``` php var_dump( \Yggverse\Net\Dig::isRecord('A') ); @@ -62,8 +62,172 @@ var_dump( #### Check record value valid -``` +``` php var_dump( \Yggverse\Net\Dig::isRecordValue('A', '127.0.0.1') ); -``` \ No newline at end of file +``` + +### Resolve + +#### Init resolver + +``` php +$resolve = new \Yggverse\Net\Resolve( + [ + 'A', + 'AAAA' + ], + [ + '1.1.1.1', + '8.8.8.8' + ], + // .. +); +``` + +#### Get resolved URL string + +``` php +$resolved = $resolve->url( + 'https://en.wikipedia.org/wiki/Domain_Name_System' + // next arguments contain debug variables and new address object +); + +if ($resolved) +{ + var_dump( + $resolved // https://185.15.59.224/wiki/Domain_Name_System + ); +} +``` + +#### Resolve Address object + +``` php +$resolved = $resolve->address( + new \Yggverse\Net\Address( + 'https://en.wikipedia.org/wiki/Domain_Name_System' + ) +); + +if ($resolved) +{ + var_dump( + $resolved->get() // https://185.15.59.224/wiki/Domain_Name_System + ); + + var_dump( + $resolved->getHost() // 185.15.59.224 + ); +} +``` + +### Address + +Includes methods to work with network addresses. + +#### Base address methods + +Different operations with address parts: + +* `scheme` +* `user` +* `password` +* `host` +* `protocol` +* `path` +* `query` +* `fragment` + +#### Address conversion from relative to absolute format + +**Document root** + +``` php +$base = new \Yggverse\Net\Address( + 'http://yo.ygg/a1/b1/c1' +); + +$address = new \Yggverse\Net\Address( + '/a2' +); + +var_dump( + $address->getAbsolute( + $base + ) // return http://yo.ygg/a2 +); +``` + +**Current folder** + +``` php +$base = new \Yggverse\Net\Address( + 'http://yo.ygg/a1/b1/c1' +); + +$address = new \Yggverse\Net\Address( + 'c2' +); + +var_dump( + $address->getAbsolute( + $base + ) // return http://yo.ygg/a1/b1/c2 +); +``` + +**Ending slash** + +``` php +$base = new \Yggverse\Net\Address( + 'http://yo.ygg/a1/b1/c1/' +); + +$address = new \Yggverse\Net\Address( + '../../b2/c2' +); + +var_dump( + $address->getAbsolute( + $base + ) // return http://yo.ygg/a1/b2/c2 +); +``` + +**All options** + +``` php +$base = new \Yggverse\Net\Address( + 'http://user:password@yo.ygg/a1/b1/c1?attribute=value#anchor' +); + +$address = new \Yggverse\Net\Address( + '../../a2/b2?attribute2=value2#anchor2' +); + +var_dump( + $address->getAbsolute( + $base + ) // return http://user:password@yo.ygg/a2/b2?attribute2=value2#anchor2 +); +``` + +### Valid + +Network entities validation + +**Supported methods** + +* `Valid::ip` +* `Valid::ip4` +* `Valid::ip6` +* `Valid::domainHostName` + +## Integrations + +* [gemini-dl](https://github.com/YGGverse/gemini-dl) - CLI batch downloader for Gemini protocol +* [web-api](https://github.com/YGGverse/web-api) - Network API with native Yggdrasil/IPv6 support +* [Yo!](https://github.com/YGGverse/Yo) - Crawler and search engine for different networks +* [Yoda](https://github.com/YGGverse/Yoda) - Experimental PHP-GTK browser for Gemini protocol diff --git a/src/Address.php b/src/Address.php new file mode 100644 index 0000000..41d19c6 --- /dev/null +++ b/src/Address.php @@ -0,0 +1,401 @@ +setScheme( + (string) $scheme + ); + } + + if ($user = parse_url($address, PHP_URL_USER)) + { + $this->setUser( + (string) $user + ); + } + + if ($pass = parse_url($address, PHP_URL_PASS)) + { + $this->setPass( + (string) $pass + ); + } + + if ($host = parse_url($address, PHP_URL_HOST)) + { + $this->setHost( + (string) $host + ); + } + + if ($port = parse_url($address, PHP_URL_PORT)) + { + $this->setPort( + (int) $port + ); + } + + if ($path = parse_url($address, PHP_URL_PATH)) + { + $this->setPath( + (string) $path + ); + } + + if ($query = parse_url($address, PHP_URL_QUERY)) + { + $this->setQuery( + (string) $query + ); + } + + if ($fragment = parse_url($address, PHP_URL_FRAGMENT)) + { + $this->setFragment( + (string) $fragment + ); + } + } + } + + public function isAbsolute(): bool + { + return boolval( + $this->_scheme || $this->_host + ); + } + + public function isRelative(): bool + { + return !$this->isAbsolute(); + } + + public function getScheme(): ?string + { + return $this->_scheme; + } + + public function setScheme(?string $value): void + { + $this->_scheme = $value; + } + + public function getHost(): ?string + { + return $this->_host; + } + + public function setHost(?string $value): void + { + $this->_host = $value; + } + + public function getUser(): ?string + { + return $this->_user; + } + + public function setUser(?string $value): void + { + $this->_user = $value; + } + + public function getPass(): ?string + { + return $this->_pass; + } + + public function setPass(?string $value): void + { + $this->_pass = $value; + } + + public function getPort(): ?int + { + return $this->_port; + } + + public function setPort(?int $value): void + { + $this->_port = $value; + } + + public function getPath(): ?string + { + return $this->_path; + } + + public function setPath(?string $value): void + { + if (false !== strpos($value, '\\')) + { + $this->setSeparator( + '\\' + ); + } + + $this->_segments = explode( + $this->_separator, + $value + ); + + $this->_path = $value; + } + + public function getSegments(): array + { + return $this->_segments; + } + + public function getQuery(): ?string + { + return $this->_query; + } + + public function setQuery(?string $value): void + { + $this->_query = $value; + } + + public function getFragment(): ?string + { + return $this->_fragment; + } + + public function setFragment(?string $value): void + { + $this->_fragment = $value; + } + + public function getSeparator(): string + { + return $this->_separator; + } + + public function setSeparator(?string $value): void + { + $this->_separator = $value; + } + + public function get( + bool $scheme = true, + bool $user = true, + bool $pass = true, + bool $host = true, + bool $port = true, + bool $path = true, + bool $query = true, + bool $fragment = true, + ): string + { + $address = ''; + + if ($scheme && $this->getScheme()) + { + $address .= sprintf( + '%s:%s%s', + $this->getScheme(), + $this->getSeparator(), + $this->getSeparator() + ); + } + + if ($user && $this->getUser()) + { + if ($pass && $this->getPass()) + { + $address .= sprintf( + '%s:%s@', + $this->getUser(), + $this->getPass() + ); + } + + else + { + $address .= sprintf( + '%s@', + $this->getUser() + ); + } + } + + if ($host && $this->getHost()) + { + $address .= $this->getHost(); + } + + if ($port && $this->getPort()) + { + $address .= sprintf( + ':%d', + $this->getPort() + ); + } + + if ($path && $this->getPath()) + { + if (!str_starts_with($this->getPath(), $this->getSeparator())) + { + $address .= $this->getSeparator(); + } + + $address .= $this->getPath(); + } + + if ($query && $this->getQuery()) + { + $address .= sprintf( + '?%s', + $this->getQuery() + ); + } + + if ($fragment && $this->getFragment()) + { + $address .= sprintf( + '#%s', + $this->getFragment() + ); + } + + return $address; + } + + public function toAbsolute( + \Yggverse\Net\Address $base + ): bool + { + if ($this->isAbsolute()) + { + return true; + } + + if ($base->isRelative()) + { + return false; + } + + $this->setScheme( + $base->getScheme() + ); + + $this->setUser( + $base->getUser() + ); + + $this->setPass( + $base->getPass() + ); + + $this->setHost( + $base->getHost() + ); + + $this->setPort( + $base->getPort() + ); + + $this->setSeparator( + $base->getSeparator() + ); + + if (str_starts_with((string) $this->getPath(), $this->getSeparator())) + { + return true; + } + + if ($path = $this->getPath()) + { + $prefix = array_reverse( + $base->getSegments() + ); + + array_shift( + $prefix + ); + + $navigate = true; + + $postfix = []; + + foreach ($this->getSegments() as $index => $segment) + { + if ($segment == '.') + { + continue; + } + + if ($navigate && $segment == '..') + { + if (empty($prefix[$index])) + { + return false; + } + + unset( + $prefix[$index] + ); + + continue; + } + + $navigate = false; + + $postfix[] = $segment; + } + + $this->setPath( + implode( + $this->getSeparator(), + array_merge( + array_reverse( + $prefix + ), + $postfix + ) + ) + ); + + return true; + } + + return false; + } + + // @TODO deprecated, legacy needs only + public function getAbsolute( + \Yggverse\Net\Address $base + ): ?string + { + if ($this->toAbsolute($base)) + { + return $this->get(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Dig.php b/src/Dig.php index 7323f0c..e8f5ae9 100644 --- a/src/Dig.php +++ b/src/Dig.php @@ -17,32 +17,59 @@ class Dig ]; } + public static function isProvider(mixed $value): bool + { + return + ( + is_string($value) && + false !== filter_var($value, FILTER_VALIDATE_IP) + ); + } + public static function isHostName(mixed $value, array $find = ['_'], array $replace = []): bool { - return is_string($value) && false !== filter_var(str_replace($find, $replace, $value), FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); + return ( + is_string($value) && + false !== filter_var(str_replace($find, $replace, $value), FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) + ); } public static function isRecord(mixed $value): bool { - return is_string($value) && isset(self::_records()[$value]); + return + ( + is_string($value) && + isset(self::_records()[$value]) + ); } public static function isRecordValue(mixed $record, mixed $value): bool { - return is_string($record) && - is_string($value) && - isset(self::_records()[$record]) && self::_records()[$record]($value); + return + ( + is_string($record) && + is_string($value) && + isset(self::_records()[$record]) && self::_records()[$record]($value) + ); } - public static function records(string $hostname, array $records, array &$result = [], array &$error = []): array + public static function records(string $hostname, array $records, array &$result = [], array &$error = [], ?string $provider = null, int $timeout = 5): array { + if (self::isProvider($provider)) + { + $provider = sprintf( + '@%s', + $provider + ); + } + if (self::isHostName($hostname)) { foreach ($records as $record) { if (self::isRecord($record)) { - if ($values = exec(sprintf('dig %s %s +short', $record, $hostname))) + if ($values = exec(sprintf('dig %s %s %s +short +time=%d', (string) $provider, $record, $hostname, $timeout))) { foreach (explode(PHP_EOL, $values) as $value) { diff --git a/src/Resolve.php b/src/Resolve.php new file mode 100644 index 0000000..16f6d7e --- /dev/null +++ b/src/Resolve.php @@ -0,0 +1,204 @@ +setRecords( + $records + ); + } + + if ($providers) + { + $this->setProviders( + $providers + ); + } + + if ($timeout) + { + $this->setTimeout( + $timeout + ); + } + + $this->setShuffle( + $shuffle + ); + } + + public function getRecords(): array + { + return $this->_records; + } + + public function setRecords(array $records): void + { + foreach ($records as $record) + { + $this->addRecord( + $record + ); + } + } + + public function addRecord(string $record): void + { + $this->_records[] = $record; + + $this->_records = array_unique( + $this->_records + ); + } + + public function getProviders(): array + { + return $this->_providers; + } + + public function setProviders(array $providers): void + { + foreach ($providers as $provider) + { + $this->addProvider( + $provider + ); + } + } + + public function addProvider(string $provider): void + { + $this->_providers[] = $provider; + + $this->_providers = array_unique( + $this->_providers + ); + } + + public function setTimeout( + int $timeout + ): void + { + $this->_timeout = $timeout; + } + + public function getTimeout(): int + { + return $this->_timeout; + } + + public function setShuffle( + bool $shuffle + ): void + { + $this->_shuffle = $shuffle; + } + + public function isShuffle(): bool + { + return $this->_shuffle; + } + + public function url( + string $url, + array &$result = [], + array &$error = [], + ?\Yggverse\Net\Address &$resolved = null + ): ?string + { + $resolved = $this->address( + new \Yggverse\Net\Address( + $url + ), + $result, + $error + ); + + if ($resolved) + { + return $resolved->get(); + } + + return null; + } + + public function address( + \Yggverse\Net\Address $address, + array &$result = [], + array &$error = [] + ): ?\Yggverse\Net\Address + { + foreach ($this->_providers as $provider) + { + foreach ( + \Yggverse\Net\Dig::records( + $address->getHost(), + $this->_records, + $result, + $error, + $provider, + $this->_timeout + ) as $record => $data + ) { + + if ($this->_shuffle) + { + shuffle( + $data + ); + } + + foreach ($data as $host) + { + if (\Yggverse\Net\Valid::ip6($host)) + { + $address->setHost( + sprintf( + '[%s]', + $host + ) + ); + } + + else + { + $address->setHost( + $host + ); + } + + return $address; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Valid.php b/src/Valid.php new file mode 100644 index 0000000..3d80ecc --- /dev/null +++ b/src/Valid.php @@ -0,0 +1,40 @@ +