Merge branch 'feature/autodns'

This commit is contained in:
Maël Gangloff
2024-10-06 16:13:29 +02:00
8 changed files with 392 additions and 14 deletions

View File

@@ -54,6 +54,7 @@ The table below lists the supported API connector providers:
| OVH | https://api.ovh.com | **Yes** | | OVH | https://api.ovh.com | **Yes** |
| GANDI | https://api.gandi.net/docs/domains/ | **Yes** | | GANDI | https://api.gandi.net/docs/domains/ | **Yes** |
| NAMECHEAP | https://www.namecheap.com/support/api/methods/domains/create/ | **Yes** | | NAMECHEAP | https://www.namecheap.com/support/api/methods/domains/create/ | **Yes** |
| AUTODNS | https://cloud.autodns.com/ | **Yes** |
If a domain has expired and a connector is linked to the Watchlist, then Domain Watchdog will try to order it via the If a domain has expired and a connector is linked to the Watchlist, then Domain Watchdog will try to order it via the
connector provider's API. connector provider's API.

View File

@@ -1,4 +1,4 @@
import {Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd"; import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
import React, {useState} from "react"; import React, {useState} from "react";
import {Connector, ConnectorProvider} from "../../../utils/api/connectors"; import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
import {t} from "ttag"; import {t} from "ttag";
@@ -132,6 +132,61 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
</Form.Item> </Form.Item>
</> </>
} }
{
provider === ConnectorProvider.AUTODNS && <>
<Alert
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
type="warning"/>
<br/>
<Form.Item
label={t`AutoDNS Username`}
name={['authData', 'username']}
help={<Typography.Text
type='secondary'>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}</Typography.Text>}
rules={[{required: true, message: t`Required`}]}>
<Input autoComplete='off' required={true}/>
</Form.Item>
<Form.Item
label={t`AutoDNS Password`}
name={['authData', 'password']}
rules={[{required: true, message: t`Required`}]}
required={true}>
<Input.Password autoComplete='off' required={true} placeholder=''/>
</Form.Item>
<Form.Item
label={t`Domain Contact Handle ID`}
name={['authData', 'contactid']}
help={<Typography.Text
type='secondary'>{t`The Contact ID for ownership of registered Domains. `}<a
href="https://cloud.autodns.com/contacts/domain">{t`You got from this page`}</a></Typography.Text>}
rules={[{required: true, message: t`Required`}]}
required={true}>
<Input autoComplete='off' required={true} placeholder=''/>
</Form.Item>
<Form.Item
label={t`Context Value`}
name={['authData', 'context']}
help={<Typography.Text
type='secondary'>{t`If you not sure, use the default value 4`}</Typography.Text>}
required={false}>
<Input autoComplete='off' required={false} placeholder='4'/>
</Form.Item>
<Form.Item
valuePropName='checked'
label={t`Owner confirmation`}
name={['authData', 'ownerConfirm']}
rules={[{required: true, message: t`Required`}]}
>
<Checkbox
required={true}>{t`Owner confirms his consent of domain order jobs`}</Checkbox>
</Form.Item>
</>
}
{ {
provider === ConnectorProvider.NAMECHEAP && <> provider === ConnectorProvider.NAMECHEAP && <>
<Form.Item <Form.Item

View File

@@ -3,6 +3,7 @@ import {request} from "./index";
export enum ConnectorProvider { export enum ConnectorProvider {
OVH = 'ovh', OVH = 'ovh',
GANDI = 'gandi', GANDI = 'gandi',
AUTODNS = 'autodns',
NAMECHEAP = 'namecheap' NAMECHEAP = 'namecheap'
} }

View File

@@ -19,6 +19,10 @@ export const helpGetTokenLink = (provider?: string) => {
return <Typography.Link target='_blank' href="https://ap.www.namecheap.com/settings/tools/apiaccess/"> return <Typography.Link target='_blank' href="https://ap.www.namecheap.com/settings/tools/apiaccess/">
{t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`} {t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`}
</Typography.Link> </Typography.Link>
case ConnectorProvider.AUTODNS:
return <Typography.Link target='_blank' href="https://en.autodns.com/domain-robot-api/">
{t`Because of some limitations in API of AutoDNS, we suggest to create an dedicated user for API with limited rights.`}
</Typography.Link>
default: default:
return <></> return <></>
@@ -31,6 +35,8 @@ export const tosHyperlink = (provider?: string) => {
return 'https://www.ovhcloud.com/fr/terms-and-conditions/contracts/' return 'https://www.ovhcloud.com/fr/terms-and-conditions/contracts/'
case ConnectorProvider.GANDI: case ConnectorProvider.GANDI:
return 'https://www.gandi.net/en/contracts/terms-of-service' return 'https://www.gandi.net/en/contracts/terms-of-service'
case ConnectorProvider.AUTODNS:
return 'https://www.internetx.com/agb/'
default: default:
return '' return ''
} }

View File

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

View File

@@ -0,0 +1,254 @@
<?php
namespace App\Service\Connector;
use App\Entity\Domain;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
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
{
public function __construct(CacheItemPoolInterface $cacheItemPool, private readonly HttpClientInterface $client)
{
parent::__construct($cacheItemPool);
}
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');
}
if ($dryRun) {
return;
}
$this->client->request(
'POST',
'/v1/domain',
(new HttpOptions())
->setAuthBasic($this->authData['username'], $this->authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $this->authData['context'])
->setBaseUri(self::BASE_URL)
->setJson([
'name' => $ldhName,
'ownerc' => [
'id' => $this->authData['contactid'],
],
'adminc' => [
'id' => $this->authData['contactid'],
],
'techc' => [
'id' => $this->authData['contactid'],
],
'confirmOrder' => $this->authData['ownerConfirm'],
'nameServers' => [
[
'name' => 'a.ns14.net',
],
[
'name' => 'b.ns14.net',
],
[
'name' => 'c.ns14.net',
],
[
'name' => 'd.ns14.net',
],
],
])
->toArray()
)->toArray();
}
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
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();
}
}
public function verifyAuthData(array $authData): 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
|| true !== $authData['ownerConfirm']
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod
) {
throw new HttpException(451, 'The user has not given explicit consent');
}
return [
'username' => $authData['username'],
'password' => $authData['password'],
'acceptConditions' => $authData['acceptConditions'],
'ownerLegalAge' => $authData['ownerLegalAge'],
'ownerConfirm' => $authData['ownerConfirm'],
'waiveRetractationPeriod' => $authData['waiveRetractationPeriod'],
'context' => $authData['context'],
];
}
public function isSupported(Domain ...$domainList): bool
{
return true;
}
protected function getSupportedTldList(): array
{
return [];
}
/**
* @throws InvalidArgumentException
*/
protected function getCachedTldList(): CacheItemInterface
{
return $this->cacheItemPool->getItem('app.provider.autodns.supported-tld');
}
/**
* @throws TransportExceptionInterface
*/
public function assertAuthentication(): void
{
try {
$response = $this->client->request(
'GET',
'/v1/hello',
(new HttpOptions())
->setAuthBasic($this->authData['username'], $this->authData['password'])
->setHeader('Accept', 'application/json')
->setHeader('X-Domainrobot-Context', $this->authData['context'])
->setBaseUri(self::BASE_URL)
->toArray()
);
} catch (\Exception) {
throw new BadRequestHttpException('Invalid Login');
}
if (Response::HTTP_OK !== $response->getStatusCode()) {
throw new BadRequestHttpException('The status of these credentials is not valid');
}
}
}

View File

@@ -20,7 +20,7 @@ class GandiProvider extends AbstractProvider
{ {
private const BASE_URL = 'https://api.gandi.net'; private const BASE_URL = 'https://api.gandi.net';
public function __construct(CacheItemPoolInterface $cacheItemPool, private HttpClientInterface $client) public function __construct(CacheItemPoolInterface $cacheItemPool, private readonly HttpClientInterface $client)
{ {
parent::__construct($cacheItemPool); parent::__construct($cacheItemPool);
} }

View File

@@ -42,9 +42,13 @@ msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:84 #: assets/components/tracking/connector/ConnectorForm.tsx:84
#: assets/components/tracking/connector/ConnectorForm.tsx:92 #: assets/components/tracking/connector/ConnectorForm.tsx:92
#: assets/components/tracking/connector/ConnectorForm.tsx:122 #: assets/components/tracking/connector/ConnectorForm.tsx:122
#: assets/components/tracking/connector/ConnectorForm.tsx:158 #: assets/components/tracking/connector/ConnectorForm.tsx:146
#: assets/components/tracking/connector/ConnectorForm.tsx:172 #: assets/components/tracking/connector/ConnectorForm.tsx:152
#: assets/components/tracking/connector/ConnectorForm.tsx:181 #: assets/components/tracking/connector/ConnectorForm.tsx:162
#: assets/components/tracking/connector/ConnectorForm.tsx:182
#: assets/components/tracking/connector/ConnectorForm.tsx:213
#: assets/components/tracking/connector/ConnectorForm.tsx:227
#: assets/components/tracking/connector/ConnectorForm.tsx:236
#: assets/components/tracking/watchlist/WatchlistForm.tsx:115 #: assets/components/tracking/watchlist/WatchlistForm.tsx:115
#: assets/components/tracking/watchlist/WatchlistForm.tsx:212 #: assets/components/tracking/watchlist/WatchlistForm.tsx:212
msgid "Required" msgid "Required"
@@ -218,48 +222,96 @@ msgid "It indicates the organization that will pay for the ordered product"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:138 #: assets/components/tracking/connector/ConnectorForm.tsx:138
msgid ""
"This provider does not provide a list of supported TLD. Please double check "
"if the domain you want to register is supported."
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:142
msgid "AutoDNS Username"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:145
msgid ""
"Attention: AutoDNS do not support 2-Factor Authentication on API Users for "
"automated systems"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:150
msgid "AutoDNS Password"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:157
msgid "Domain Contact Handle ID"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:160
msgid "The Contact ID for ownership of registered Domains. "
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:161
msgid "You got from this page"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:168
msgid "Context Value"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:171
msgid "If you not sure, use the default value 4"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:179
msgid "Owner confirmation"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:185
msgid "Owner confirms his consent of domain order jobs"
msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:193
#: assets/pages/UserPage.tsx:18 #: assets/pages/UserPage.tsx:18
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:144 #: assets/components/tracking/connector/ConnectorForm.tsx:199
msgid "API key" msgid "API key"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:156 #: assets/components/tracking/connector/ConnectorForm.tsx:211
msgid "API Terms of Service" msgid "API Terms of Service"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:164 #: assets/components/tracking/connector/ConnectorForm.tsx:219
msgid "" msgid ""
"I have read and accepted the conditions of use of the Provider API, " "I have read and accepted the conditions of use of the Provider API, "
"accessible from this hyperlink" "accessible from this hyperlink"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:170 #: assets/components/tracking/connector/ConnectorForm.tsx:225
msgid "Legal age" msgid "Legal age"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:175 #: assets/components/tracking/connector/ConnectorForm.tsx:230
msgid "I am of the minimum age required to consent to these conditions" msgid "I am of the minimum age required to consent to these conditions"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:179 #: assets/components/tracking/connector/ConnectorForm.tsx:234
msgid "Withdrawal period" msgid "Withdrawal period"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:184 #: assets/components/tracking/connector/ConnectorForm.tsx:239
msgid "" msgid ""
"I waive my right of withdrawal regarding the purchase of domain names via " "I waive my right of withdrawal regarding the purchase of domain names via "
"the Provider's API" "the Provider's API"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:192 #: assets/components/tracking/connector/ConnectorForm.tsx:247
#: assets/components/tracking/watchlist/WatchlistForm.tsx:252 #: assets/components/tracking/watchlist/WatchlistForm.tsx:252
msgid "Create" msgid "Create"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:195 #: assets/components/tracking/connector/ConnectorForm.tsx:250
#: assets/components/tracking/watchlist/WatchlistForm.tsx:255 #: assets/components/tracking/watchlist/WatchlistForm.tsx:255
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
@@ -1047,6 +1099,12 @@ msgid ""
"website" "website"
msgstr "" msgstr ""
#: assets/utils/providers/index.tsx:24
msgid ""
"Because of some limitations in API of AutoDNS, we suggest to create an "
"dedicated user for API with limited rights."
msgstr ""
#: assets/utils/providers/ovh.tsx:5 #: assets/utils/providers/ovh.tsx:5
msgid "Application key" msgid "Application key"
msgstr "" msgstr ""