Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,28 @@ $response = $newRequest->getResponse();

See [Response/Definitions](src/Response/Definitions) for more details about code

### Using your own HTTP client and cache

By default, requests are made with `file_get_contents()` and the IANA bootstrap files are cached in the system temporary directory.

The client optionally accepts a [PSR-18](https://www.php-fig.org/psr/psr-18/) HTTP client, a [PSR-17](https://www.php-fig.org/psr/psr-17/) request factory and a [PSR-16](https://www.php-fig.org/psr/psr-16/) cache. The HTTP client is used for both the bootstrap files and the RDAP queries, so timeouts, proxies, retries and logging follow your application's configuration. The cache stores the bootstrap files for `AbstractRdapService::cacheExpirations()` seconds.

```php
use ArrayAccess\RdapClient\Client;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;

// Psr18Client is also a request factory, so the second argument can be omitted
$httpClient = new Psr18Client(HttpClient::create(['timeout' => 5]));
$cache = new Psr16Cache(new FilesystemAdapter());
$client = new Client($httpClient, null, $cache);

// any PSR-18 client works, e.g. Guzzle with a PSR-17 factory
$client = new Client(new \GuzzleHttp\Client(['timeout' => 5]), new \GuzzleHttp\Psr7\HttpFactory());
```


## Note

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"require": {
"php": ">= 8.1",
"ext-intl": "*",
"ext-json": "*"
"ext-json": "*",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.11.2",
Expand All @@ -46,4 +49,4 @@
"ArrayAccess\\RdapClient\\" : "src/"
}
}
}
}
140 changes: 139 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,43 @@

use ArrayAccess\RdapClient\Exceptions\EmptyArgumentException;
use ArrayAccess\RdapClient\Exceptions\InvalidServiceDefinitionException;
use ArrayAccess\RdapClient\Exceptions\RdapRemoteRequestException;
use ArrayAccess\RdapClient\Exceptions\UnsupportedProtocolException;
use ArrayAccess\RdapClient\Interfaces\RdapClientInterface;
use ArrayAccess\RdapClient\Interfaces\RdapHttpClientAwareInterface;
use ArrayAccess\RdapClient\Interfaces\RdapProtocolInterface;
use ArrayAccess\RdapClient\Interfaces\RdapRequestInterface;
use ArrayAccess\RdapClient\Protocols\AsnProtocol;
use ArrayAccess\RdapClient\Protocols\DomainProtocol;
use ArrayAccess\RdapClient\Protocols\IPv4Protocol;
use ArrayAccess\RdapClient\Protocols\IPv6Protocol;
use ArrayAccess\RdapClient\Protocols\NsProtocol;
use ArrayAccess\RdapClient\Services\AsnService;
use ArrayAccess\RdapClient\Util\CIDR;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\SimpleCache\CacheInterface;
use function explode;
use function file_get_contents;
use function idn_to_ascii;
use function is_a;
use function is_array;
use function is_int;
use function is_object;
use function is_string;
use function preg_match;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function str_contains;
use function stream_context_create;
use function strlen;
use function strtolower;
use function trim;

class Client implements RdapClientInterface
class Client implements RdapClientInterface, RdapHttpClientAwareInterface
{
/**
* @var string Version
Expand All @@ -51,6 +64,131 @@ class Client implements RdapClientInterface
*/
protected array $protocols = self::PROTOCOLS;

/**
* @var RequestFactoryInterface|null $requestFactory The PSR-17 request factory
*/
protected ?RequestFactoryInterface $requestFactory;

/**
* Constructor
*
* Without arguments, requests are made with file_get_contents() and the
* IANA bootstrap files are cached in the system temporary directory.
*
* @param ClientInterface|null $httpClient PSR-18 client used for bootstrap and RDAP requests
* @param RequestFactoryInterface|null $requestFactory PSR-17 request factory,
* optional when the HTTP client also implements RequestFactoryInterface
* @param CacheInterface|null $cache PSR-16 cache for the IANA bootstrap files
* @throws InvalidArgumentException if an HTTP client is given without a request factory
*/
public function __construct(
protected ?ClientInterface $httpClient = null,
?RequestFactoryInterface $requestFactory = null,
protected ?CacheInterface $cache = null
) {
if ($requestFactory === null && $httpClient instanceof RequestFactoryInterface) {
$requestFactory = $httpClient;
}
if ($httpClient !== null && $requestFactory === null) {
throw new InvalidArgumentException(
'A PSR-17 request factory is required when using a PSR-18 HTTP client'
);
}
$this->requestFactory = $requestFactory;
}

/**
* @inheritDoc
*/
public function getHttpClient() : ?ClientInterface
{
return $this->httpClient;
}

/**
* Get the PSR-17 request factory
*
* @return RequestFactoryInterface|null
*/
public function getRequestFactory() : ?RequestFactoryInterface
{
return $this->requestFactory;
}

/**
* @inheritDoc
*/
public function getCache() : ?CacheInterface
{
return $this->cache;
}

/**
* @inheritDoc
*/
public function fetch(string $url) : string
{
if ($this->httpClient === null || $this->requestFactory === null) {
return $this->fetchWithStream($url);
}

$request = $this->requestFactory
->createRequest('GET', $url)
->withHeader('Accept', 'application/rdap+json,application/json;q=0.9,*/*;q=0.8')
->withHeader('User-Agent', 'Rdap-Client/' . self::VERSION);
try {
$response = $this->httpClient->sendRequest($request);
} catch (ClientExceptionInterface $e) {
throw new RdapRemoteRequestException(
$e->getMessage(),
(int) $e->getCode(),
$e
);
}

$content = (string) $response->getBody();
if ($content === '') {
throw new RdapRemoteRequestException(
sprintf('Could not get RDAP response from %s', $url),
$response->getStatusCode()
);
}
return $content;
}

/**
* Fetch the body of the given URL with file_get_contents()
*
* @param string $url
* @return string
* @throws RdapRemoteRequestException
*/
protected function fetchWithStream(string $url) : string
{
$errorCode = 0;
$errorMessage = null;
set_error_handler(
static function (int $code, string $message) use (&$errorCode, &$errorMessage) : bool {
$errorCode = $code;
$errorMessage = $message;
return true;
}
);
$content = file_get_contents(
$url,
false,
stream_context_create(RdapRequestInterface::DEFAULT_STREAM_CONTEXT)
);
restore_error_handler();
if (!is_string($content) || $content === '') {
throw new RdapRemoteRequestException(
$errorMessage ?? sprintf('Could not get RDAP response from %s', $url),
$errorCode
);
}
return $content;
}

/**
* Check if protocol is supported
*
Expand Down
36 changes: 36 additions & 0 deletions src/Interfaces/RdapHttpClientAwareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);

namespace ArrayAccess\RdapClient\Interfaces;

use Psr\Http\Client\ClientInterface;
use Psr\SimpleCache\CacheInterface;

interface RdapHttpClientAwareInterface
{
/**
* Get the PSR-18 HTTP client
*
* @return ClientInterface|null null when the built-in stream wrapper is used
*/
public function getHttpClient() : ?ClientInterface;

/**
* Get the PSR-16 cache used for the IANA bootstrap files
*
* @return CacheInterface|null null when the built-in temporary file cache is used
*/
public function getCache() : ?CacheInterface;

/**
* Fetch the body of the given URL
*
* The body is returned regardless of the HTTP status code,
* so RDAP error responses (RFC 9083 section 6) can be parsed.
*
* @param string $url
* @return string
* @throws \ArrayAccess\RdapClient\Exceptions\RdapRemoteRequestException
*/
public function fetch(string $url) : string;
}
44 changes: 44 additions & 0 deletions src/Protocols/AbstractRdapProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

namespace ArrayAccess\RdapClient\Protocols;

use ArrayAccess\RdapClient\Exceptions\InvalidServiceDefinitionException;
use ArrayAccess\RdapClient\Exceptions\RdapServerNotFoundException;
use ArrayAccess\RdapClient\Interfaces\RdapClientInterface;
use ArrayAccess\RdapClient\Interfaces\RdapHttpClientAwareInterface;
use ArrayAccess\RdapClient\Interfaces\RdapProtocolInterface;
use ArrayAccess\RdapClient\Interfaces\RdapRequestInterface;
use ArrayAccess\RdapClient\Interfaces\RdapServiceInterface;
use ArrayAccess\RdapClient\Services\AbstractRdapService;
use function explode;
use function is_string;
use function md5;
use function sprintf;
use function str_contains;

Expand Down Expand Up @@ -39,6 +44,45 @@ public function getClient(): RdapClientInterface
return $this->client;
}

/**
* Load the service from an IANA bootstrap URL
*
* Uses the PSR-18 HTTP client and PSR-16 cache of the client when configured,
* otherwise falls back to {@see AbstractRdapService::fromURL()}.
*
* @template T of AbstractRdapService
* @param class-string<T> $serviceClass
* @param string $url
* @return T
* @throws \Exception
* @noinspection PhpFullyQualifiedNameUsageInspection
*/
protected function loadService(string $serviceClass, string $url): AbstractRdapService
{
$client = $this->getClient();
if (!$client instanceof RdapHttpClientAwareInterface
|| ($client->getHttpClient() === null && $client->getCache() === null)
) {
return $serviceClass::fromURL($url);
}

$cache = $client->getCache();
$cacheKey = 'rdap-client.bootstrap.' . md5($url);
$content = $cache?->get($cacheKey);
if (is_string($content)) {
try {
return $serviceClass::fromJson($content, $url);
} catch (InvalidServiceDefinitionException) {
$cache?->delete($cacheKey);
}
}

$content = $client->fetch($url);
$service = $serviceClass::fromJson($content, $url);
$cache?->set($cacheKey, $content, $serviceClass::cacheExpirations());
return $service;
}

/**
* @inheritDoc
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Protocols/AsnProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function __construct(RdapClientInterface $client, ?AsnService $service =
public function getService(): AsnService
{
if (!isset($this->services) || !$this->services instanceof AsnService) {
$this->services = AsnService::fromURL(self::ASN_URI);
$this->services = $this->loadService(AsnService::class, self::ASN_URI);
}
return $this->services;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Protocols/DomainProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function setService(DomainService $service): void
public function getService(): DomainService
{
if (!isset($this->services) || !($this->services instanceof DomainService)) {
$this->services = DomainService::fromURL(self::DOMAIN_URI);
$this->services = $this->loadService(DomainService::class, self::DOMAIN_URI);
}
return $this->services;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Protocols/IPv4Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function setService(Ipv4Service $service): void
public function getService(): Ipv4Service
{
if (!isset($this->services) || !($this->services instanceof Ipv4Service)) {
$this->services = Ipv4Service::fromURL(self::IPV4_URI);
$this->services = $this->loadService(Ipv4Service::class, self::IPV4_URI);
}
return $this->services;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Protocols/IPv6Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function setService(Ipv6Service $service): void
public function getService(): Ipv6Service
{
if (!isset($this->services) || !($this->services instanceof Ipv6Service)) {
$this->services = Ipv6Service::fromURL(self::IPV6_URI);
$this->services = $this->loadService(Ipv6Service::class, self::IPV6_URI);
}
return $this->services;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Protocols/NsProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function setService(NsService $service): void
public function getService(): NsService
{
if (!isset($this->services) || !($this->services instanceof NsService)) {
$this->services = NsService::fromURL(self::NS_URI);
$this->services = $this->loadService(NsService::class, self::NS_URI);
}
return $this->services;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Protocols/RdapRequestProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use ArrayAccess\RdapClient\Exceptions\MismatchProtocolBehaviorException;
use ArrayAccess\RdapClient\Exceptions\RdapRemoteRequestException;
use ArrayAccess\RdapClient\Interfaces\RdapHttpClientAwareInterface;
use ArrayAccess\RdapClient\Interfaces\RdapProtocolInterface;
use ArrayAccess\RdapClient\Interfaces\RdapRequestInterface;
use ArrayAccess\RdapClient\Interfaces\RdapResponseInterface;
Expand Down Expand Up @@ -158,6 +159,17 @@ public function getResponse(): RdapResponseInterface
$this->errorCode
);
}
$client = $this->getProtocol()->getClient();
if ($client instanceof RdapHttpClientAwareInterface && $client->getHttpClient() !== null) {
try {
$content = $client->fetch($this->rdapSearchURL);
} catch (RdapRemoteRequestException $e) {
$this->errorCode = $e->getCode();
$this->errorMessage = $e->getMessage();
throw $e;
}
return $this->response = $this->protocol->createResponse($content, $this);
}
set_error_handler(function (int $code, string $message, string $file, int $line) : bool {
$this->errorCode = $code;
$this->errorMessage = $message;
Expand Down
Loading
Loading