From b10c92e14e6bc28dfd5e7ca235fc4a2a521f8272 Mon Sep 17 00:00:00 2001 From: Unit 193 Date: Thu, 10 Apr 2025 05:29:27 -0400 Subject: New upstream version 2.1.1. --- .github/workflows/docs_publish.yml | 2 +- .github/workflows/docs_source_update.yml | 2 +- .github/workflows/formatting_check.yml | 2 +- .github/workflows/linting_check.yml | 2 +- .github/workflows/pypi-publish-release.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- Makefile | 92 +++++++++++------ docs/source/index.rst | 2 +- docs/source/pkb_client.cli.rst | 4 +- docs/source/pkb_client.client.rst | 20 ++-- docs/source/pkb_client.rst | 2 +- pkb_client/__init__.py | 2 +- pkb_client/client/client.py | 113 +++++++++++++++++++- pkb_client/client/dnssec.py | 35 +++++++ requirements.txt | 4 +- setup.py | 2 +- tests/client.py | 161 ++++++++++++++++++++++++++++- 17 files changed, 391 insertions(+), 58 deletions(-) create mode 100644 pkb_client/client/dnssec.py diff --git a/.github/workflows/docs_publish.yml b/.github/workflows/docs_publish.yml index af146b2..9b6a687 100644 --- a/.github/workflows/docs_publish.yml +++ b/.github/workflows/docs_publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: 3.12 diff --git a/.github/workflows/docs_source_update.yml b/.github/workflows/docs_source_update.yml index c80ee0b..f90a4c3 100644 --- a/.github/workflows/docs_source_update.yml +++ b/.github/workflows/docs_source_update.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: 3.12 diff --git a/.github/workflows/formatting_check.yml b/.github/workflows/formatting_check.yml index fadbadd..7501ad0 100644 --- a/.github/workflows/formatting_check.yml +++ b/.github/workflows/formatting_check.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/linting_check.yml b/.github/workflows/linting_check.yml index e223b36..113da7f 100644 --- a/.github/workflows/linting_check.yml +++ b/.github/workflows/linting_check.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pypi-publish-release.yml b/.github/workflows/pypi-publish-release.yml index 5150277..e89dcd3 100644 --- a/.github/workflows/pypi-publish-release.yml +++ b/.github/workflows/pypi-publish-release.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: 3.9 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 53e60e1..32899c2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.5.0 with: python-version: ${{ matrix.python-version }} diff --git a/Makefile b/Makefile index 6d2ca3d..fe53262 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ RELEASE = 1 RESULT_PATH = target RPMBUILD_PATH = ~/rpmbuild +#------------------------------------------------------------------------------ +# COMMANDS +#------------------------------------------------------------------------------ + all: help help: @@ -29,42 +33,66 @@ build-srpm: ${RESULT_PATH}/python3-pkb-client-${VERSION}-${RELEASE}.src.rpm build-rpm: ${RESULT_PATH}/python3-pkb-client-${VERSION}-${RELEASE}.noarch.rpm -# file generators + +#------------------------------------------------------------------------------ +# FILE GENERATORs +#------------------------------------------------------------------------------ + +define _spec_generator +cat << EOF +%global modname pkb_client + +Name: python3-pkb-client +Version: ${VERSION} +Release: ${RELEASE} +Obsoletes: %{name} <= %{version} +Summary: Python client for the Porkbun API + +License: MIT License +URL: https://github.com/infinityofspace/pkb_client/ +Source0: %{name}-%{version}.tar.xz + +Requires: python3-requests +Requires: python3-dns +Requires: python3-responses + +BuildArch: noarch +BuildRequires: python3-setuptools +BuildRequires: python3-rpm-macros +BuildRequires: python3-py + +%?python_enable_dependency_generator + +%description +Python client for the Porkbun API + +%%prep +%autosetup -n %{modname}_v%{version} + +%build +%py3_build + +%install +%py3_install + +%files +%doc Readme.md +%license License +%{_bindir}/pkb-client +%{python3_sitelib}/%{modname}/ +%{python3_sitelib}/%{modname}-%{version}* + +%changelog +... + +EOF +endef +export spec_generator = $(value _spec_generator) python3-pkb-client.spec: @mkdir -p ${RESULT_PATH}/ @printf '[INFO] generating python3-pkb-client.spec\n' | tee -a ${RESULT_PATH}/build.log - @printf '%%global modname pkb_client\n\n' > python3-pkb-client.spec - @printf 'Name: python3-pkb-client\n' >> python3-pkb-client.spec - @printf 'Version: '${VERSION}'\n' >> python3-pkb-client.spec - @printf 'Release: '${RELEASE}'\n' >> python3-pkb-client.spec - @printf 'Obsoletes: %%{name} <= %%{version}\n' >> python3-pkb-client.spec - @printf 'Summary: Python client for the Porkbun API\n\n' >> python3-pkb-client.spec - @printf 'License: MIT License\n' >> python3-pkb-client.spec - @printf 'URL: https://github.com/infinityofspace/pkb_client/\n' >> python3-pkb-client.spec - @printf 'Source0: %%{name}-%%{version}.tar.xz\n\n' >> python3-pkb-client.spec - @printf 'BuildArch: noarch\n' >> python3-pkb-client.spec - @printf 'BuildRequires: python3-setuptools\n' >> python3-pkb-client.spec - @printf 'BuildRequires: python3-rpm-macros\n' >> python3-pkb-client.spec - @printf 'BuildRequires: python3-py\n\n' >> python3-pkb-client.spec - @printf '%%?python_enable_dependency_generator\n\n' >> python3-pkb-client.spec - @printf '%%description\n' >> python3-pkb-client.spec - @printf 'Python client for the Porkbun API\n\n' >> python3-pkb-client.spec - @printf '%%prep\n' >> python3-pkb-client.spec - @printf '%%autosetup -n %%{modname}_v%%{version}\n\n' >> python3-pkb-client.spec - @printf '%%build\n' >> python3-pkb-client.spec - @printf '%%py3_build\n\n' >> python3-pkb-client.spec - @printf '%%install\n' >> python3-pkb-client.spec - @printf '%%py3_install\n\n' >> python3-pkb-client.spec - @printf '%%files\n' >> python3-pkb-client.spec - @printf '%%doc Readme.md\n' >> python3-pkb-client.spec - @printf '%%license License\n' >> python3-pkb-client.spec - @printf '%%{_bindir}/pkb-client\n' >> python3-pkb-client.spec - @printf '%%{python3_sitelib}/%%{modname}/\n' >> python3-pkb-client.spec - @printf '%%{python3_sitelib}/%%{modname}-%%{version}*\n\n' >> python3-pkb-client.spec - @printf '%%changelog\n' >> python3-pkb-client.spec - @printf '...\n' >> python3-pkb-client.spec - @printf '\n' >> python3-pkb-client.spec + @ VERSION=${VERSION} RELEASE=${RELEASE} eval "$$spec_generator" > python3-pkb-client.spec ${RESULT_PATH}/python3-pkb-client-${VERSION}.tar.xz: @mkdir -p ${RESULT_PATH}/ diff --git a/docs/source/index.rst b/docs/source/index.rst index 46f8eb1..d113178 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,7 +14,7 @@ About +++++ *pkb_client* is a python client for the `Porkbun `_ API. It supports the v3 of the API. You can -find the official documentation of the Porkbun API `here `_. +find the official documentation of the Porkbun API `here `_. Link to the source code: `Github `_ diff --git a/docs/source/pkb_client.cli.rst b/docs/source/pkb_client.cli.rst index 29fc20f..95bd2b2 100644 --- a/docs/source/pkb_client.cli.rst +++ b/docs/source/pkb_client.cli.rst @@ -9,13 +9,13 @@ pkb\_client.cli.cli module .. automodule:: pkb_client.cli.cli :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: pkb_client.cli :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/source/pkb_client.client.rst b/docs/source/pkb_client.client.rst index fd677b1..8e3d2c5 100644 --- a/docs/source/pkb_client.client.rst +++ b/docs/source/pkb_client.client.rst @@ -9,53 +9,61 @@ pkb\_client.client.bind\_file module .. automodule:: pkb_client.client.bind_file :members: - :undoc-members: :show-inheritance: + :undoc-members: pkb\_client.client.client module -------------------------------- .. automodule:: pkb_client.client.client :members: - :undoc-members: :show-inheritance: + :undoc-members: pkb\_client.client.dns module ----------------------------- .. automodule:: pkb_client.client.dns :members: + :show-inheritance: :undoc-members: + +pkb\_client.client.dnssec module +-------------------------------- + +.. automodule:: pkb_client.client.dnssec + :members: :show-inheritance: + :undoc-members: pkb\_client.client.domain module -------------------------------- .. automodule:: pkb_client.client.domain :members: - :undoc-members: :show-inheritance: + :undoc-members: pkb\_client.client.forwarding module ------------------------------------ .. automodule:: pkb_client.client.forwarding :members: - :undoc-members: :show-inheritance: + :undoc-members: pkb\_client.client.ssl\_cert module ----------------------------------- .. automodule:: pkb_client.client.ssl_cert :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: pkb_client.client :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/source/pkb_client.rst b/docs/source/pkb_client.rst index 161588e..6ed42de 100644 --- a/docs/source/pkb_client.rst +++ b/docs/source/pkb_client.rst @@ -15,5 +15,5 @@ Module contents .. automodule:: pkb_client :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/pkb_client/__init__.py b/pkb_client/__init__.py index d5096ef..93d9382 100644 --- a/pkb_client/__init__.py +++ b/pkb_client/__init__.py @@ -1 +1 @@ -__version__ = "v2.0.0" +__version__ = "v2.1.1" diff --git a/pkb_client/client/client.py b/pkb_client/client/client.py index 86956a5..81b946b 100644 --- a/pkb_client/client/client.py +++ b/pkb_client/client/client.py @@ -2,7 +2,7 @@ import json import logging from hashlib import sha256 from pathlib import Path -from typing import Optional, List, Union +from typing import List, Optional, Union from urllib.parse import urljoin import dns.resolver @@ -10,11 +10,12 @@ import requests from pkb_client.client import BindFile from pkb_client.client.dns import ( + DNS_RECORDS_WITH_PRIORITY, DNSRecord, - DNSRestoreMode, DNSRecordType, - DNS_RECORDS_WITH_PRIORITY, + DNSRestoreMode, ) +from pkb_client.client.dnssec import DNSSECRecord from pkb_client.client.domain import DomainInfo from pkb_client.client.forwarding import URLForwarding, URLForwardingType from pkb_client.client.ssl_cert import SSLCertBundle @@ -842,6 +843,112 @@ class PKBClient: response_json.get("message", "Unknown message"), ) + def get_dnssec_records(self, domain: str) -> List[DNSSECRecord]: + """ + API DNSSEC retrieve method: retrieve all DNSSEC records for the given domain. + See https://porkbun.com/api/json/v3/documentation#DNSSEC%20Get%20Records for more info. + + :param domain: the domain for which the DNSSEC records should be retrieved + :return: list of :class:`DNSSECRecord` objects + """ + + url = urljoin(self.api_endpoint, f"dns/getDnssecRecords/{domain}") + req_json = self._get_auth_request_json() + r = requests.post(url=url, json=req_json) + + if r.status_code == 200: + return [ + DNSSECRecord.from_dict(record) + for record in json.loads(r.text).get("records", {}).values() + ] + else: + response_json = json.loads(r.text) + raise PKBClientException( + response_json.get("status", "Unknown status"), + response_json.get("message", "Unknown message"), + ) + + def create_dnssec_record( + self, + domain: str, + key_tag: int, + alg: int, + digest_type: int, + digest: str, + max_sig_life: Optional[int] = None, + key_data_flags: Optional[int] = None, + key_data_protocol: Optional[int] = None, + key_data_algo: Optional[int] = None, + key_data_pub_key: Optional[str] = None, + ) -> bool: + """ + API DNSSEC create method: create a new DNSSEC record for the given domain. + See https://porkbun.com/api/json/v3/documentation#DNSSEC%20Create%20Record for more info. + + :param domain: the domain for which the DNSSEC record should be created + :param key_tag: the key tag of the DNSSEC record + :param alg: algorithm of the DNSSEC record + :param digest_type: digest type of the DNSSEC record + :param digest: digest of the DNSSEC record + :param max_sig_life: maximum signature life of the DNSSEC record in seconds + :param key_data_flags: key data flags of the DNSSEC record + :param key_data_protocol: key data protocol of the DNSSEC record + :param key_data_algo: key data algorithm of the DNSSEC record + :param key_data_pub_key: key data public key of the DNSSEC record + + :return: True if everything went well + """ + + if max_sig_life is not None and max_sig_life < 0: + raise ValueError("max_sig_life must be greater than 0") + + url = urljoin(self.api_endpoint, f"dns/createDnssecRecord/{domain}") + req_json = { + **self._get_auth_request_json(), + "keyTag": key_tag, + "alg": alg, + "digestType": digest_type, + "digest": digest, + "maxSigLife": max_sig_life, + "keyDataFlags": key_data_flags, + "keyDataProtocol": key_data_protocol, + "keyDataAlgo": key_data_algo, + "keyDataPubKey": key_data_pub_key, + } + r = requests.post(url=url, json=req_json) + + if r.status_code == 200: + return True + else: + response_json = json.loads(r.text) + raise PKBClientException( + response_json.get("status", "Unknown status"), + response_json.get("message", "Unknown message"), + ) + + def delete_dnssec_record(self, domain: str, key_tag: int) -> bool: + """ + API DNSSEC delete method: delete an existing DNSSEC record for the given domain. + See https://porkbun.com/api/json/v3/documentation#DNSSEC%20Delete%20Record for more info. + + :param domain: the domain for which the DNSSEC record should be deleted + :param key_tag: the key tag of the DNSSEC record + :return: True if everything went well + """ + + url = urljoin(self.api_endpoint, f"dns/deleteDnssecRecord/{domain}/{key_tag}") + req_json = self._get_auth_request_json() + r = requests.post(url=url, json=req_json) + + if r.status_code == 200: + return True + else: + response_json = json.loads(r.text) + raise PKBClientException( + response_json.get("status", "Unknown status"), + response_json.get("message", "Unknown message"), + ) + @staticmethod def __handle_error_backup__(dns_records): # merge the single DNS records into one single dict with the record id as key diff --git a/pkb_client/client/dnssec.py b/pkb_client/client/dnssec.py new file mode 100644 index 0000000..2f68ecb --- /dev/null +++ b/pkb_client/client/dnssec.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class DNSSECRecord: + key_tag: int # The key tag is a 16-bit integer that identifies the DNSKEY record + alg: int # Indicates the algorithm used to generate the public key + digest_type: int # Indicates the type of digest algorithm used + digest: str # The digest of the public key + max_sig_life: Optional[ + int + ] # Indicates the amount of time in seconds the signature is valid + key_data_flags: Optional[ + int + ] # Indicates the key type (Zone-signing or Key-signing) + key_data_protocol: Optional[int] # Indicates the protocol used for the key + key_data_algo: Optional[int] # Indicates the algorithm used for the key + key_data_pub_key: Optional[str] # The public key in base64 format + + @staticmethod + def from_dict(d): + return DNSSECRecord( + key_tag=int(d["keyTag"]), + alg=int(d["alg"]), + digest_type=int(d["digestType"]), + digest=d["digest"], + max_sig_life=int(d["maxSigLife"]) if "maxSigLife" in d else None, + key_data_flags=int(d["keyDataFlags"]) if "keyDataFlags" in d else None, + key_data_protocol=int(d["keyDataProtocol"]) + if "keyDataProtocol" in d + else None, + key_data_algo=int(d["keyDataAlgo"]) if "keyDataAlgo" in d else None, + key_data_pub_key=d["keyDataPubKey"] if "keyDataPubKey" in d else None, + ) diff --git a/requirements.txt b/requirements.txt index b6b0625..e129f40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ setuptools>=39.0.1 requests>=2.20.0 sphinx~=7.4 dnspython~=2.7 -responses~=0.25.3 -ruff~=0.7 +responses~=0.25.7 +ruff~=0.11 diff --git a/setup.py b/setup.py index d1c8f04..eaaa9d2 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( ], packages=find_packages(), python_requires=">=3.9", - install_requires=["setuptools>=39.0.1", "requests>=2.20.0", "dnspython~=2.6"], + install_requires=["setuptools>=39.0.1", "requests>=2.20.0", "dnspython~=2.7"], entry_points={ "console_scripts": [ "pkb-client = pkb_client.cli.cli:main", diff --git a/tests/client.py b/tests/client.py index 9362be2..3b3a813 100644 --- a/tests/client.py +++ b/tests/client.py @@ -9,13 +9,14 @@ from responses import matchers from responses.registries import OrderedRegistry from pkb_client.client import ( - PKBClient, - PKBClientException, API_ENDPOINT, DNSRestoreMode, + PKBClient, + PKBClientException, + SSLCertBundle, ) -from pkb_client.client import SSLCertBundle from pkb_client.client.dns import DNSRecord, DNSRecordType +from pkb_client.client.dnssec import DNSSECRecord from pkb_client.client.forwarding import URLForwarding, URLForwardingType @@ -1020,6 +1021,160 @@ class TestClientAuth(unittest.TestCase): pkb_client.import_bind_dns_records(filename, DNSRestoreMode.clear) + @responses.activate + def test_get_dnssec_records(self): + pkb_client = PKBClient("key", "secret") + + responses.post( + url=urljoin(API_ENDPOINT, "dns/getDnssecRecords/example.com"), + json={ + "status": "SUCCESS", + "records": { + "12345": { + "keyTag": "12345", + "alg": "8", + "digestType": "1", + "digest": "abc123", + }, + "12346": { + "keyTag": "12346", + "alg": "8", + "digestType": "1", + "digest": "abc456", + "maxSigLife": 3600, + "keyDataFlags": 257, + "keyDataProtocol": 3, + "keyDataAlgo": 8, + "keyDataPubKey": "abc789", + }, + }, + }, + match=[ + matchers.json_params_matcher( + {"apikey": "key", "secretapikey": "secret"} + ) + ], + ) + dnssec_records = pkb_client.get_dnssec_records("example.com") + + self.assertEqual(2, len(dnssec_records)) + self.assertEqual( + DNSSECRecord( + key_tag=12345, + alg=8, + digest_type=1, + digest="abc123", + max_sig_life=None, + key_data_flags=None, + key_data_protocol=None, + key_data_algo=None, + key_data_pub_key=None, + ), + dnssec_records[0], + ) + self.assertEqual( + DNSSECRecord( + key_tag=12346, + alg=8, + digest_type=1, + digest="abc456", + max_sig_life=3600, + key_data_flags=257, + key_data_protocol=3, + key_data_algo=8, + key_data_pub_key="abc789", + ), + dnssec_records[1], + ) + + @responses.activate + def test_create_dnssec_record(self): + pkb_client = PKBClient("key", "secret") + + responses.post( + url=urljoin(API_ENDPOINT, "dns/createDnssecRecord/example.com"), + json={"status": "SUCCESS"}, + match=[ + matchers.json_params_matcher( + { + "apikey": "key", + "secretapikey": "secret", + "keyTag": 4242, + "alg": 12345, + "digestType": 8, + "digest": "abc123", + "maxSigLife": None, + "keyDataFlags": None, + "keyDataProtocol": None, + "keyDataAlgo": None, + "keyDataPubKey": None, + } + ) + ], + ) + + success = pkb_client.create_dnssec_record( + domain="example.com", + key_tag=4242, + alg=12345, + digest_type=8, + digest="abc123", + ) + self.assertTrue(success) + + responses.post( + url=urljoin(API_ENDPOINT, "dns/createDnssecRecord/example2.com"), + json={"status": "SUCCESS"}, + match=[ + matchers.json_params_matcher( + { + "apikey": "key", + "secretapikey": "secret", + "keyTag": 4242, + "alg": 12345, + "digestType": 8, + "digest": "abc123", + "maxSigLife": 42, + "keyDataFlags": 41, + "keyDataProtocol": 40, + "keyDataAlgo": 39, + "keyDataPubKey": "abc42", + } + ) + ], + ) + + success = pkb_client.create_dnssec_record( + domain="example2.com", + key_tag=4242, + alg=12345, + digest_type=8, + digest="abc123", + max_sig_life=42, + key_data_flags=41, + key_data_protocol=40, + key_data_algo=39, + key_data_pub_key="abc42", + ) + self.assertTrue(success) + + @responses.activate + def delete_dnssec_record(self): + pkb_client = PKBClient("key", "secret") + + responses.post( + url=urljoin(API_ENDPOINT, "dns/deleteDnssecRecord/example.com/123456"), + json={"status": "SUCCESS"}, + match=[ + matchers.json_params_matcher( + {"apikey": "key", "secretapikey": "secret"} + ) + ], + ) + + success = pkb_client.delete_dnssec_record("example.com", 123456) + self.assertTrue(success) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3