chore: merge master

This commit is contained in:
Maël Gangloff
2024-10-02 12:59:37 +02:00
22 changed files with 1302 additions and 985 deletions

View File

@@ -2,15 +2,17 @@
namespace App\Config;
use App\Config\Provider\AutodnsProvider;
use App\Config\Provider\GandiProvider;
use App\Config\Provider\OvhProvider;
use App\Service\Connector\AutodnsProvider;
use App\Service\Connector\GandiProvider;
use App\Service\Connector\NamecheapProvider;
use App\Service\Connector\OvhProvider;
enum ConnectorProvider: string
{
case OVH = 'ovh';
case GANDI = 'gandi';
case AUTODNS = 'autodns';
case NAMECHEAP = 'namecheap';
public function getConnectorProvider(): string
{
@@ -18,6 +20,7 @@ enum ConnectorProvider: string
ConnectorProvider::OVH => OvhProvider::class,
ConnectorProvider::GANDI => GandiProvider::class,
ConnectorProvider::AUTODNS => AutodnsProvider::class,
ConnectorProvider::NAMECHEAP => NamecheapProvider::class,
};
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace App\Config\Provider;
use App\Entity\Domain;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
abstract class AbstractProvider
{
public function __construct(
protected array $authData,
protected HttpClientInterface $client,
protected CacheItemPoolInterface $cacheItemPool,
protected KernelInterface $kernel
) {
}
abstract public static function verifyAuthData(array $authData, HttpClientInterface $client): array;
abstract public function orderDomain(Domain $domain, bool $dryRun): void;
public function isSupported(Domain ...$domainList): bool
{
$item = $this->getCachedTldList();
if (!$item->isHit()) {
$supportedTldList = $this->getSupportedTldList();
$item
->set($supportedTldList)
->expiresAfter(new \DateInterval('PT1H'));
$this->cacheItemPool->saveDeferred($item);
} else {
$supportedTldList = $item->get();
}
$extensionList = [];
foreach ($domainList as $domain) {
// We want to check the support of TLDs and SLDs here.
// For example, it is not enough for the Connector to support .fr for it to support the domain name example.asso.fr.
// It must support .asso.fr.
$extension = explode('.', $domain->getLdhName(), 2)[1];
if (!in_array($extension, $extensionList)) {
$extensionList[] = $extension;
}
}
foreach ($extensionList as $extension) {
if (!in_array($extension, $supportedTldList)) {
return false;
}
}
return true;
}
abstract protected function getCachedTldList(): CacheItemInterface;
abstract protected function getSupportedTldList(): array;
}

View File

@@ -1,234 +0,0 @@
<?php
namespace App\Config\Provider;
use App\Entity\Domain;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AutodnsProvider extends AbstractProvider
{
private const BASE_URL = 'https://api.autodns.com';
/**
* Order a domain name with the Gandi API.
*
* @throws \Exception
* @throws TransportExceptionInterface
* @throws DecodingExceptionInterface
*/
public function orderDomain(Domain $domain, bool $dryRun = false): void
{
if (!$domain->getDeleted()) {
throw new \InvalidArgumentException('The domain name still appears in the WHOIS database');
}
$ldhName = $domain->getLdhName();
if (!$ldhName) {
throw new \InvalidArgumentException('Domain name cannot be null');
}
$authData = self::verifyAuthData($this->authData, $this->client);
if($dryRun) {
return;
}
$this->client->request(
'POST',
'/v1/domain',
(new HttpOptions())
->setAuthBasic($authData['username'], $authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $authData['context'])
->setBaseUri(self::BASE_URL)
->setJson([
'name' => $ldhName,
'ownerc' => [
'id' => $authData['contactid'],
],
'adminc' => [
'id' => $authData['contactid'],
],
'techc' => [
'id' => $authData['contactid'],
],
'confirmOrder' => $authData['ownerConfirm'],
'nameServers' => [
[
'name' => 'a.ns14.net'
],
[
'name' => 'b.ns14.net'
],
[
'name' => 'c.ns14.net'
],
[
'name' => 'd.ns14.net'
],
]
])
->toArray()
)->toArray();
}
public function registerZone(Domain $domain, bool $dryRun = false): void
{
$authData = $this->authData;
$ldhName = $domain->getLdhName();
if($dryRun) {
return;
}
$zoneCheck = $this->client->request(
'POST',
'/v1/zone/_search?keys=name',
(new HttpOptions())
->setAuthBasic($authData['username'], $authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $authData['context'])
->setBaseUri(self::BASE_URL)
->setJson([
'filters' => [
[
'key' => 'name',
'value' => $ldhName,
'operator' => 'EQUAL',
]
],
])
->toArray()
)->toArray();
$responseDataIsEmpty = empty($zoneCheck['data']);
if ($responseDataIsEmpty) {
// The domain not yet exists in DNS Server, we create them
$this->client->request(
'POST',
'/v1/zone',
(new HttpOptions())
->setAuthBasic($authData['username'], $authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $authData['context'])
->setBaseUri(self::BASE_URL)
->setJson([
'origin' => $ldhName,
'main' => [
'address' => $authData['dns_ip']
],
'soa' => [
'refresh' => 3600,
'retry' => 7200,
'expire' => 604800,
'ttl' => 600
],
'action' => 'COMPLETE',
'wwwInclude' => true,
'nameServers' => [
[
'name' => 'a.ns14.net'
],
[
'name' => 'b.ns14.net'
],
[
'name' => 'c.ns14.net'
],
[
'name' => 'd.ns14.net'
],
]
])
->toArray()
)->toArray();
}
}
/**
* @throws TransportExceptionInterface
*/
public static function verifyAuthData(array $authData, HttpClientInterface $client): array
{
$username = $authData['username'];
$password = $authData['password'];
$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
$waiveRetractationPeriod = $authData['waiveRetractationPeriod'];
if (empty($authData['context'])) {
$authData['context'] = 4;
}
if (
empty($username) || empty($password)
) {
throw new BadRequestHttpException('Bad authData schema');
}
if (
true !== $acceptConditions
|| empty($authData['ownerConfirm'])
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod
) {
throw new HttpException(451, 'The user has not given explicit consent');
}
try {
$response = $client->request(
'GET',
'/v1/hello',
(new HttpOptions())
->setAuthBasic($authData['username'], $authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $authData['context'])
->setBaseUri(self::BASE_URL)
->toArray()
);
} catch (\Exception $exp) {
throw new BadRequestHttpException('Invalid Login');
}
if (Response::HTTP_OK !== $response->getStatusCode()) {
throw new BadRequestHttpException('The status of these credentials is not valid');
}
return $authData;
}
public function isSupported(Domain ...$domainList): bool
{
return true;
}
protected function getSupportedTldList(): array {
return [];
}
/**
* @throws \Psr\Cache\InvalidArgumentException
*/
protected function getCachedTldList(): CacheItemInterface
{
return $this->cacheItemPool->getItem('app.provider.autodns.supported-tld');
}
}

View File

@@ -1,160 +0,0 @@
<?php
namespace App\Config\Provider;
use App\Entity\Domain;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class GandiProvider extends AbstractProvider
{
private const BASE_URL = 'https://api.gandi.net';
/**
* Order a domain name with the Gandi API.
*
* @throws \Exception
* @throws TransportExceptionInterface
* @throws DecodingExceptionInterface
*/
public function orderDomain(Domain $domain, bool $dryRun = false): void
{
if (!$domain->getDeleted()) {
throw new \InvalidArgumentException('The domain name still appears in the WHOIS database');
}
$ldhName = $domain->getLdhName();
if (!$ldhName) {
throw new \InvalidArgumentException('Domain name cannot be null');
}
$authData = self::verifyAuthData($this->authData, $this->client);
$user = $this->client->request('GET', '/v5/organization/user-info', (new HttpOptions())
->setAuthBearer($authData['token'])
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->toArray()
)->toArray();
$httpOptions = (new HttpOptions())
->setAuthBearer($authData['token'])
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->setHeader('Dry-Run', $dryRun ? '1' : '0')
->setJson([
'fqdn' => $ldhName,
'owner' => [
'email' => $user['email'],
'given' => $user['firstname'],
'family' => $user['lastname'],
'streetaddr' => $user['streetaddr'],
'zip' => $user['zip'],
'city' => $user['city'],
'state' => $user['state'],
'phone' => $user['phone'],
'country' => $user['country'],
'type' => 'individual',
],
'tld_period' => 'golive',
]);
if (array_key_exists('sharingId', $authData)) {
$httpOptions->setQuery([
'sharing_id' => $authData['sharingId'],
]);
}
$res = $this->client->request('POST', '/domain/domains', $httpOptions->toArray());
if ((!$dryRun && Response::HTTP_ACCEPTED !== $res->getStatusCode())
|| ($dryRun && Response::HTTP_OK !== $res->getStatusCode())) {
throw new \HttpException($res->toArray()['message']);
}
}
/**
* @throws TransportExceptionInterface
*/
public static function verifyAuthData(array $authData, HttpClientInterface $client): array
{
$token = $authData['token'];
$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
$waiveRetractationPeriod = $authData['waiveRetractationPeriod'];
if (!is_string($token) || empty($token)
|| (array_key_exists('sharingId', $authData) && !is_string($authData['sharingId']))
) {
throw new BadRequestHttpException('Bad authData schema');
}
if (true !== $acceptConditions
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod) {
throw new HttpException(451, 'The user has not given explicit consent');
}
$response = $client->request('GET', '/v5/organization/user-info', (new HttpOptions())
->setAuthBearer($token)
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->toArray()
);
if (Response::HTTP_OK !== $response->getStatusCode()) {
throw new BadRequestHttpException('The status of these credentials is not valid');
}
$authDataReturned = [
'token' => $token,
'acceptConditions' => $acceptConditions,
'ownerLegalAge' => $ownerLegalAge,
'waiveRetractationPeriod' => $waiveRetractationPeriod,
];
if (array_key_exists('sharingId', $authData)) {
$authDataReturned['sharingId'] = $authData['sharingId'];
}
return $authDataReturned;
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
protected function getSupportedTldList(): array
{
$authData = self::verifyAuthData($this->authData, $this->client);
$response = $this->client->request('GET', '/v5/domain/tlds', (new HttpOptions())
->setAuthBearer($authData['token'])
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->toArray())->toArray();
return array_map(fn ($tld) => $tld['name'], $response);
}
/**
* @throws \Psr\Cache\InvalidArgumentException
*/
protected function getCachedTldList(): CacheItemInterface
{
return $this->cacheItemPool->getItem('app.provider.ovh.supported-tld');
}
}

View File

@@ -1,242 +0,0 @@
<?php
namespace App\Config\Provider;
use App\Entity\Domain;
use GuzzleHttp\Exception\ClientException;
use Ovh\Api;
use Ovh\Exceptions\InvalidParameterException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class OvhProvider extends AbstractProvider
{
public const REQUIRED_ROUTES = [
[
'method' => 'GET',
'path' => '/domain/extensions',
],
[
'method' => 'GET',
'path' => '/order/cart',
],
[
'method' => 'GET',
'path' => '/order/cart/*',
],
[
'method' => 'POST',
'path' => '/order/cart',
],
[
'method' => 'POST',
'path' => '/order/cart/*',
],
[
'method' => 'DELETE',
'path' => '/order/cart/*',
],
];
/**
* Order a domain name with the OVH API.
*
* @throws \Exception
*/
public function orderDomain(Domain $domain, bool $dryRun = false): void
{
if (!$domain->getDeleted()) {
throw new \InvalidArgumentException('The domain name still appears in the WHOIS database');
}
$ldhName = $domain->getLdhName();
if (!$ldhName) {
throw new \InvalidArgumentException('Domain name cannot be null');
}
$authData = self::verifyAuthData($this->authData, $this->client);
$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
$waiveRetractationPeriod = $authData['waiveRetractationPeriod'];
$conn = new Api(
$authData['appKey'],
$authData['appSecret'],
$authData['apiEndpoint'],
$authData['consumerKey']
);
$cart = $conn->post('/order/cart', [
'ovhSubsidiary' => $authData['ovhSubsidiary'],
'description' => 'Domain Watchdog',
]);
$cartId = $cart['cartId'];
$offers = $conn->get("/order/cart/{$cartId}/domain", [
'domain' => $ldhName,
]);
$pricingModes = ['create-default'];
if ('create-default' !== $authData['pricingMode']) {
$pricingModes[] = $authData['pricingMode'];
}
$offer = array_filter($offers, fn ($offer) => 'create' === $offer['action']
&& true === $offer['orderable']
&& in_array($offer['pricingMode'], $pricingModes)
);
if (empty($offer)) {
$conn->delete("/order/cart/{$cartId}");
throw new \InvalidArgumentException('Cannot buy this domain name');
}
$item = $conn->post("/order/cart/{$cartId}/domain", [
'domain' => $ldhName,
'duration' => 'P1Y',
]);
$itemId = $item['itemId'];
// $conn->get("/order/cart/{$cartId}/summary");
$conn->post("/order/cart/{$cartId}/assign");
$conn->get("/order/cart/{$cartId}/item/{$itemId}/requiredConfiguration");
$configuration = [
'ACCEPT_CONDITIONS' => $acceptConditions,
'OWNER_LEGAL_AGE' => $ownerLegalAge,
];
foreach ($configuration as $label => $value) {
$conn->post("/order/cart/{$cartId}/item/{$itemId}/configuration", [
'cartId' => $cartId,
'itemId' => $itemId,
'label' => $label,
'value' => $value,
]);
}
$conn->get("/order/cart/{$cartId}/checkout");
if ($dryRun) {
return;
}
$conn->post("/order/cart/{$cartId}/checkout", [
'autoPayWithPreferredPaymentMethod' => true,
'waiveRetractationPeriod' => $waiveRetractationPeriod,
]);
}
/**
* @throws \Exception
*/
public static function verifyAuthData(array $authData, HttpClientInterface $client): array
{
$appKey = $authData['appKey'];
$appSecret = $authData['appSecret'];
$apiEndpoint = $authData['apiEndpoint'];
$consumerKey = $authData['consumerKey'];
$ovhSubsidiary = $authData['ovhSubsidiary'];
$pricingMode = $authData['pricingMode'];
$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
$waiveRetractationPeriod = $authData['waiveRetractationPeriod'];
if (!is_string($appKey) || empty($appKey)
|| !is_string($appSecret) || empty($appSecret)
|| !is_string($consumerKey) || empty($consumerKey)
|| !is_string($apiEndpoint) || empty($apiEndpoint)
|| !is_string($ovhSubsidiary) || empty($ovhSubsidiary)
|| !is_string($pricingMode) || empty($pricingMode)
) {
throw new BadRequestHttpException('Bad authData schema');
}
if (true !== $acceptConditions
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod) {
throw new HttpException(451, 'The user has not given explicit consent');
}
$conn = new Api(
$appKey,
$appSecret,
$apiEndpoint,
$consumerKey
);
try {
$res = $conn->get('/auth/currentCredential');
if (null !== $res['expiration'] && new \DateTime($res['expiration']) < new \DateTime()) {
throw new BadRequestHttpException('These credentials have expired');
}
$status = $res['status'];
if ('validated' !== $status) {
throw new BadRequestHttpException("The status of these credentials is not valid ($status)");
}
} catch (ClientException $exception) {
throw new BadRequestHttpException($exception->getMessage());
}
foreach (self::REQUIRED_ROUTES as $requiredRoute) {
$ok = false;
foreach ($res['rules'] as $allowedRoute) {
if (
$requiredRoute['method'] === $allowedRoute['method']
&& fnmatch($allowedRoute['path'], $requiredRoute['path'])
) {
$ok = true;
}
}
if (!$ok) {
throw new BadRequestHttpException('This Connector does not have enough permissions on the Provider API. Please recreate this Connector.');
}
}
return [
'appKey' => $appKey,
'appSecret' => $appSecret,
'apiEndpoint' => $apiEndpoint,
'consumerKey' => $consumerKey,
'ovhSubsidiary' => $ovhSubsidiary,
'pricingMode' => $pricingMode,
'acceptConditions' => $acceptConditions,
'ownerLegalAge' => $ownerLegalAge,
'waiveRetractationPeriod' => $waiveRetractationPeriod,
];
}
/**
* @throws InvalidParameterException
* @throws \JsonException
* @throws \Exception
*/
protected function getSupportedTldList(): array
{
$authData = self::verifyAuthData($this->authData, $this->client);
$conn = new Api(
$authData['appKey'],
$authData['appSecret'],
$authData['apiEndpoint'],
$authData['consumerKey']
);
return $conn->get('/domain/extensions', [
'ovhSubsidiary' => $authData['ovhSubsidiary'],
]);
}
/**
* @throws InvalidArgumentException
*/
protected function getCachedTldList(): CacheItemInterface
{
return $this->cacheItemPool->getItem('app.provider.ovh.supported-tld');
}
}