Skip to content

Commit

Permalink
Merge cb39af9 into 6973a76
Browse files Browse the repository at this point in the history
  • Loading branch information
ssorensen committed Apr 19, 2023
2 parents 6973a76 + cb39af9 commit ffc1c59
Show file tree
Hide file tree
Showing 32 changed files with 5,443 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -29,6 +29,7 @@ lexicon/providers/dnsimple.py @analogj
lexicon/providers/dnsmadeeasy.py @analogj @nydr
lexicon/providers/dnspark.py @analogj
lexicon/providers/dnspod.py @analogj
lexicon/providers/dnsservices.py @ssorensen
lexicon/providers/dreamhost.py @chhsiao1981 @ryan953
lexicon/providers/dynu.py @HerrFolgreich
lexicon/providers/easydns.py @analogj
Expand Down
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -135,6 +135,7 @@ The current supported providers are:
.. _dnsmadeeasy: https://api-docs.dnsmadeeasy.com/?version=latest
.. _dnspark: https://dnspark.zendesk.com/entries/31210577-rest-api-dns-documentation
.. _dnspod: https://support.dnspod.cn/support/api
.. _dnsservices: https://dns.services/userapi
.. _dreamhost: https://help.dreamhost.com/hc/en-us/articles/217560167-api_overview
.. _dynu: https://www.dynu.com/support/api
.. _easydns: http://docs.sandbox.rest.easydns.net/
Expand Down
4 changes: 4 additions & 0 deletions docs/providers/dnsservices.rst
@@ -0,0 +1,4 @@
dnsservices
* ``auth_username`` Specify email address for authentication

* ``auth_password`` Specify password for authentication
3 changes: 3 additions & 0 deletions docs/providers_options.rst
Expand Up @@ -59,6 +59,9 @@ List of options
.. _dnspod:
.. include:: providers/dnspod.rst

.. _dnsservices:
.. include:: providers/dnsservices.rst

.. _dreamhost:
.. include:: providers/dreamhost.rst

Expand Down
168 changes: 168 additions & 0 deletions lexicon/providers/dnsservices.py
@@ -0,0 +1,168 @@
"""Module provider for DNS.services"""
import json
import logging

import requests

from lexicon.providers.base import Provider as BaseProvider
from lexicon.exceptions import AuthenticationError

LOGGER = logging.getLogger(__name__)

NAMESERVER_DOMAINS = ["dns.services"]


def provider_parser(subparser):
"""Configure provider parser for DNS.services"""
subparser.add_argument("--auth-username", help="specify username for authentication")
subparser.add_argument("--auth-password", help="specify password for authentication")


class Provider(BaseProvider):
"""Provider class for DNS.services"""

def __init__(self, config):
super(Provider, self).__init__(config)
self.auth_token = None
self.domain_id = None
self.api_endpoint = "https://dns.services/api"

def _authenticate(self):
if self.auth_token is None:
username = self._get_provider_option("auth_username")
password = self._get_provider_option("auth_password")
data = {
"username": username,
"password": password,
}

result = requests.post(self.api_endpoint + "/login", data=data)
result.raise_for_status()
self.auth_token = result.json()["token"]

data = self._get("/dns")
for zone in data["zones"]:
if zone["name"] == self.domain:
self.domain_id = zone["domain_id"]
self.service_id = zone["service_id"]
return
raise AuthenticationError("No domain found")

def _create_record(self, rtype, name, content):
print("create_record")
# check if record already exists
if not self._list_records(rtype, name, content):
data = {
"type": rtype,
"name": self._relative_name(name),
"content": content,
}
ttl = self._get_lexicon_option("ttl")
if ttl:
data["ttl"] = ttl
priority = self._get_lexicon_option("priority")
if priority:
data["priority"] = str(priority)
LOGGER.debug("create_record: %s", data)
self._post(f"/service/{self.service_id}/dns/{self.domain_id}/records", data)
return True

# List all records. Return an empty list if no records found
# type, name and content are used to filter records.
# If possible filter during the query, otherwise filter after response is received.
def _list_records(self, rtype=None, name=None, content=None):
print("list_records")
payload = self._get(f"/service/{self.service_id}/dns/{self.domain_id}")
records = []
for _, record in payload["records"].items():
processed_record = {
"type": record["type"],
"name": record['name'],
"ttl": record["ttl"],
"content": record["content"],
"id": record["id"],
}
processed_record = self._clean_TXT_record(processed_record)
records.append(processed_record)

if rtype:
records = [record for record in records if record["type"] == rtype]
if name:
records = [
record for record in records if record["name"] == self._full_name(name)
]
if content:
records = [
record
for record in records
if record["content"].lower() == content.lower()
]

LOGGER.debug("list_records: %s", records)
return records

# Update a record.
def _update_record(self, identifier, rtype=None, name=None, content=None):
print("update_record")
data = {
"type": rtype,
"name": self._relative_name(name),
"content": content,
}
ttl = self._get_lexicon_option("ttl")
if ttl:
data["ttl"] = ttl
priority = self._get_lexicon_option("priority")
if priority:
data["priority"] = str(priority)
LOGGER.debug("update_record: %s", data)
self._put(f"/service/{self.service_id}/dns/{self.domain_id}/records/{identifier}", data)
return True

# Delete an existing record.
# If record does not exist, do nothing.
def _delete_record(self, identifier=None, rtype=None, name=None, content=None):
delete_record_id = []
if not identifier:
records = self._list_records(rtype, name, content)
delete_record_id = [record["id"] for record in records]
else:
delete_record_id.append(identifier)

LOGGER.debug("delete_records: %s", delete_record_id)

for record_id in delete_record_id:
self._delete(f"/service/{self.service_id}/dns/{self.domain_id}/records/{record_id}")

# is always True at this point, if a non 200 response is returned an error is raised.
LOGGER.debug("delete_record: %s", True)
return True

# Helpers

def _request(self, action="GET", url="/", data=None, query_params=None):
print("request")
if data is None:
data = {}
if query_params is None:
query_params = {}
default_headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {self.auth_token}",
}
if not url.startswith(self.api_endpoint):
url = self.api_endpoint + url

response = requests.request(
action,
url,
params=query_params,
data=json.dumps(data),
headers=default_headers,
)
# if the request fails for any reason, throw an error.
response.raise_for_status()
if action == "DELETE":
return ""
return response.json()
25 changes: 25 additions & 0 deletions lexicon/tests/providers/test_dnsservices.py
@@ -0,0 +1,25 @@
"""Integration tests for DNS.services"""
import re
from unittest import TestCase
from lexicon.tests.providers.integration_tests import IntegrationTestsV2


# Hook into testing framework by inheriting unittest.TestCase and reuse
# the tests which *each and every* implementation of the interface must
# pass, by inheritance from define_tests.TheTests
class DNSservicesProviderTests(TestCase, IntegrationTestsV2):
"""TestCase for DNS.services"""

provider_name = "dnsservices"
domain = "astylos.dk"

def _filter_headers(self):
return [("Authorization", "Bearer TOKEN")]

def _filter_post_data_parameters(self):
return [("username", "USERNAME"), ("password", "PASSWORD")]

def _filter_response(self, response):
response["body"]["string"] = re.sub(b'"token":"[^"]+"', b'"token":"TOKEN"', response["body"]["string"])
response["body"]["string"] = re.sub(b'"refresh":"[^"]+"', b'"refresh":"REFRESH"', response["body"]["string"])
return response
@@ -0,0 +1,86 @@
interactions:
- request:
body: username=USERNAME&password=PASSWORD
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '53'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- python-requests/2.28.2
method: POST
uri: https://dns.services/api/login
response:
body:
string: '{"token":"TOKEN","refresh":"REFRESH"}'
headers:
Connection:
- keep-alive
Content-Length:
- '2060'
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 14 Apr 2023 12:56:44 GMT
Server:
- nginx
Strict-Transport-Security:
- ': max-age=31536000'
X-Frame-Options:
- SAMEORIGIN
X-RateLimit-Limit:
- '25'
X-RateLimit-Remaining:
- '21'
X-RateLimit-Reset:
- '145'
status:
code: 200
message: OK
- request:
body: '{}'
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer TOKEN
Connection:
- keep-alive
Content-Length:
- '2'
Content-Type:
- application/json
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://dns.services/api/dns
response:
body:
string: '{"service_ids":["292"],"zones":[{"domain_id":"1178","name":"astylos.dk","service_id":"292"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '93'
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 14 Apr 2023 12:56:45 GMT
Server:
- nginx
Strict-Transport-Security:
- ': max-age=31536000'
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1
@@ -0,0 +1,86 @@
interactions:
- request:
body: username=USERNAME&password=PASSWORD
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '53'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- python-requests/2.28.2
method: POST
uri: https://dns.services/api/login
response:
body:
string: '{"token":"TOKEN","refresh":"REFRESH"}'
headers:
Connection:
- keep-alive
Content-Length:
- '2060'
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 14 Apr 2023 12:56:45 GMT
Server:
- nginx
Strict-Transport-Security:
- ': max-age=31536000'
X-Frame-Options:
- SAMEORIGIN
X-RateLimit-Limit:
- '25'
X-RateLimit-Remaining:
- '20'
X-RateLimit-Reset:
- '144'
status:
code: 200
message: OK
- request:
body: '{}'
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer TOKEN
Connection:
- keep-alive
Content-Length:
- '2'
Content-Type:
- application/json
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://dns.services/api/dns
response:
body:
string: '{"service_ids":["292"],"zones":[{"domain_id":"1178","name":"astylos.dk","service_id":"292"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '93'
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 14 Apr 2023 12:56:45 GMT
Server:
- nginx
Strict-Transport-Security:
- ': max-age=31536000'
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

0 comments on commit ffc1c59

Please sign in to comment.