certapi 0.4.3__tar.gz → 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {certapi-0.4.3 → certapi-0.4.4}/PKG-INFO +1 -1
- {certapi-0.4.3 → certapi-0.4.4}/setup.py +1 -1
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/__init__.py +2 -2
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/AcmeError.py +3 -9
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/ChallengeSolver.py +4 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +7 -1
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +1 -1
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +35 -5
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +38 -9
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +31 -4
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +35 -10
- certapi-0.4.4/src/certapi/errors.py +17 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/PKG-INFO +1 -1
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/SOURCES.txt +1 -0
- {certapi-0.4.3 → certapi-0.4.4}/MANIFEST.in +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/README.md +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/pyproject.toml +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/setup.cfg +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Acme.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Challenge.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Order.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/http.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/client/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/crypto.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/client.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/types.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/FileSystemKeystore.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/manager/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/manager/acme_cert_manager.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/__init__.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/api.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/cert_api.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/key_api.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/util.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi/utils.py +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .acme.Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
|
|
2
2
|
from .manager.acme_cert_manager import AcmeCertManager
|
|
3
3
|
from .http.types import CertificateResponse, IssuedCert
|
|
4
|
-
|
|
4
|
+
from .errors import CertApiException
|
|
5
5
|
from .crypto import (
|
|
6
6
|
Certificate,
|
|
7
7
|
CertificateSigningRequest,
|
|
@@ -20,4 +20,4 @@ from .challenge_solver import (
|
|
|
20
20
|
CloudflareChallengeSolver,
|
|
21
21
|
DigitalOceanChallengeSolver,
|
|
22
22
|
)
|
|
23
|
-
from .issuers import SelfCertIssuer
|
|
23
|
+
from .issuers import CertIssuer,SelfCertIssuer,AcmeCertIssuer
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import requests
|
|
3
|
+
from certapi.errors import CertApiException
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
class AcmeError(
|
|
6
|
+
class AcmeError(CertApiException):
|
|
6
7
|
def __init__(self, message, detail, step):
|
|
7
|
-
super().__init__(
|
|
8
|
-
self.message: str = message
|
|
9
|
-
self.step: str = step
|
|
10
|
-
self.detail: dict = detail
|
|
8
|
+
super().__init__(message, detail, step)
|
|
11
9
|
self.can_retry = False
|
|
12
10
|
|
|
13
|
-
def json_obj(self) -> dict:
|
|
14
|
-
return {"message": self.message, "step": self.step, "detail": self.detail}
|
|
15
|
-
|
|
16
11
|
|
|
17
12
|
class AcmeNetworkError(AcmeError, requests.RequestException):
|
|
18
13
|
"""
|
|
@@ -23,7 +18,6 @@ class AcmeNetworkError(AcmeError, requests.RequestException):
|
|
|
23
18
|
# Initialize both parent classes
|
|
24
19
|
requests.RequestException.__init__(self, request=request)
|
|
25
20
|
AcmeError.__init__(self, message, detail, step)
|
|
26
|
-
requests.RequestException.__init__(self, request=request) # Pass message to RequestException
|
|
27
21
|
self.can_retry = True
|
|
28
22
|
|
|
29
23
|
|
|
@@ -29,6 +29,10 @@ class ChallengeSolver(ABC):
|
|
|
29
29
|
def supported_challenge_type(self) -> Literal["http-01", "dns-01", "tls-alpn-01"]:
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def cleanup_old_challenges(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
32
36
|
def __iter__(self):
|
|
33
37
|
raise NotImplementedError("Must implement `__iter__` method.")
|
|
34
38
|
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
|
|
6
6
|
class FilesystemChallengeSolver(ChallengeSolver):
|
|
7
7
|
"""
|
|
8
|
-
Filesystem implementation of the
|
|
8
|
+
Filesystem implementation of the ChallengeSolver.
|
|
9
9
|
This should never be used, but extended for your webserver e.g. nginx, apache etc.
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -36,6 +36,12 @@ class FilesystemChallengeSolver(ChallengeSolver):
|
|
|
36
36
|
def supported_challenge_type(self) -> Literal["http-01"]:
|
|
37
37
|
return "http-01"
|
|
38
38
|
|
|
39
|
+
def cleanup_old_challenges(self):
|
|
40
|
+
for filename in os.listdir(self.directory):
|
|
41
|
+
file_path = os.path.join(self.directory, filename)
|
|
42
|
+
if os.path.isfile(file_path):
|
|
43
|
+
os.remove(file_path)
|
|
44
|
+
|
|
39
45
|
def __iter__(self):
|
|
40
46
|
return (f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f)))
|
|
41
47
|
|
|
@@ -2,8 +2,10 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from collections.abc import MutableMapping
|
|
4
4
|
from typing import Literal
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
6
|
from ...ChallengeSolver import ChallengeSolver
|
|
6
7
|
from .cloudflare_client import Cloudflare
|
|
8
|
+
from certapi.errors import CertApiException
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class CloudflareChallengeSolver(ChallengeSolver):
|
|
@@ -22,7 +24,10 @@ class CloudflareChallengeSolver(ChallengeSolver):
|
|
|
22
24
|
try:
|
|
23
25
|
self.cloudflare.determine_registered_domain(domain)
|
|
24
26
|
return True
|
|
25
|
-
except
|
|
27
|
+
except CertApiException as e:
|
|
28
|
+
# Log the exception or handle it as needed, but return False as it doesn't support the domain
|
|
29
|
+
print(f"CloudflareChallengeSolver.supports_domain: {e.message} - {e.detail}")
|
|
30
|
+
return False
|
|
26
31
|
return False
|
|
27
32
|
|
|
28
33
|
def save_challenge(self, key: str, value: str, domain=None):
|
|
@@ -32,7 +37,7 @@ class CloudflareChallengeSolver(ChallengeSolver):
|
|
|
32
37
|
|
|
33
38
|
record_id = self.cloudflare.create_record(name=key, data=value, domain=base_domain)
|
|
34
39
|
self.challenges_map[key] = record_id
|
|
35
|
-
print(f"
|
|
40
|
+
print(f"CloudflareChallengeSolver[{domain}]: Added Record {key}")
|
|
36
41
|
|
|
37
42
|
def get_challenge(self, key: str, domain: str) -> str:
|
|
38
43
|
base_domain = self.cloudflare.determine_registered_domain(domain)
|
|
@@ -40,17 +45,42 @@ class CloudflareChallengeSolver(ChallengeSolver):
|
|
|
40
45
|
for record in records:
|
|
41
46
|
if record["name"] == key:
|
|
42
47
|
return record["content"]
|
|
43
|
-
return None # Return None if not found, as per
|
|
48
|
+
return None # Return None if not found, as per ChallengeSolver's __getitem__ behavior
|
|
44
49
|
|
|
45
50
|
def delete_challenge(self, key: str, domain: str):
|
|
46
51
|
if key not in self.challenges_map:
|
|
47
|
-
|
|
52
|
+
print(f"CloudflareChallengeSolver.delete: Not found Skipping key={key} record_id={record_id}")
|
|
53
|
+
return
|
|
48
54
|
|
|
49
55
|
record_id = self.challenges_map[key]
|
|
50
56
|
base_domain = self.cloudflare.determine_registered_domain(domain)
|
|
51
57
|
self.cloudflare.delete_record(record=record_id, domain=base_domain)
|
|
52
58
|
del self.challenges_map[key]
|
|
53
|
-
print(f"
|
|
59
|
+
print(f"CloudflareChallengeSolver: Deleted challenge for {key} with record ID {record_id}")
|
|
60
|
+
|
|
61
|
+
def _cleanup_zone_challenges(self, zone):
|
|
62
|
+
zone_name = zone["name"]
|
|
63
|
+
try:
|
|
64
|
+
# List all TXT records in the zone
|
|
65
|
+
records = self.cloudflare.list_txt_records(zone_name)
|
|
66
|
+
for record in records:
|
|
67
|
+
if record["type"] == "TXT" and record["name"].startswith("_acme-challenge"):
|
|
68
|
+
print(f"CloudflareChallengeSolver: Deleting old challenge record {record['name']} in zone {zone_name}")
|
|
69
|
+
try:
|
|
70
|
+
self.cloudflare.delete_record(record["id"], zone_name)
|
|
71
|
+
except CertApiException as e:
|
|
72
|
+
print(f"CloudflareChallengeSolver: Warning - Failed to delete record {record['name']} in zone {zone_name}: {e.message} - {e.detail}")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"CloudflareChallengeSolver: Warning - An unexpected error occurred while deleting record {record['name']} in zone {zone_name}: {e}")
|
|
75
|
+
except CertApiException as e:
|
|
76
|
+
print(f"CloudflareChallengeSolver: Error listing challenges in zone {zone_name}: {e.message} - {e.detail}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"CloudflareChallengeSolver: An unexpected error occurred while listing challenges in zone {zone_name}: {e}")
|
|
79
|
+
|
|
80
|
+
def cleanup_old_challenges(self):
|
|
81
|
+
zones = self.cloudflare._get_zones()
|
|
82
|
+
with ThreadPoolExecutor(max_workers=len(zones)) as executor:
|
|
83
|
+
executor.map(self._cleanup_zone_challenges, zones)
|
|
54
84
|
|
|
55
85
|
def __iter__(self):
|
|
56
86
|
# This is tricky as we can't easily iterate all challenges across all domains
|
{certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py
RENAMED
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import time
|
|
3
3
|
from os import getenv
|
|
4
4
|
from urllib.request import urlopen, Request
|
|
5
|
+
from certapi.errors import CertApiException
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class Cloudflare(object):
|
|
@@ -13,7 +14,7 @@ class Cloudflare(object):
|
|
|
13
14
|
if not self.token:
|
|
14
15
|
self.token = getenv("CLOUDFLARE_API_KEY")
|
|
15
16
|
if not self.token:
|
|
16
|
-
raise
|
|
17
|
+
raise CertApiException("CLOUDFLARE_API_KEY not found in environment", step="Cloudflare.__init__")
|
|
17
18
|
|
|
18
19
|
self._zones_cache = None
|
|
19
20
|
self._zones_cache_time = 0 # Unix timestamp of last cache update
|
|
@@ -31,7 +32,11 @@ class Cloudflare(object):
|
|
|
31
32
|
api_url = "{0}/zones?per_page=50".format(self.api)
|
|
32
33
|
response = urlopen(Request(api_url, headers=request_headers))
|
|
33
34
|
if response.getcode() != 200:
|
|
34
|
-
raise
|
|
35
|
+
raise CertApiException(
|
|
36
|
+
"Cloudflare API error",
|
|
37
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
38
|
+
step="Cloudflare._get_zones"
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
zones = json.loads(response.read().decode("utf8"))["result"]
|
|
37
42
|
self._zones_cache = zones
|
|
@@ -44,7 +49,11 @@ class Cloudflare(object):
|
|
|
44
49
|
for zone in zones:
|
|
45
50
|
if zone["name"] == domain:
|
|
46
51
|
return zone["id"]
|
|
47
|
-
raise
|
|
52
|
+
raise CertApiException(
|
|
53
|
+
"No Cloudflare zone found for domain",
|
|
54
|
+
detail={"domain": domain},
|
|
55
|
+
step="Cloudflare._get_zone_id"
|
|
56
|
+
)
|
|
48
57
|
|
|
49
58
|
def determine_registered_domain(self, domain: str) -> str:
|
|
50
59
|
"""
|
|
@@ -62,9 +71,13 @@ class Cloudflare(object):
|
|
|
62
71
|
err = e
|
|
63
72
|
continue
|
|
64
73
|
if err:
|
|
65
|
-
raise err
|
|
74
|
+
raise CertApiException(str(err), step="Cloudflare.determine_registered_domain")
|
|
66
75
|
else:
|
|
67
|
-
raise
|
|
76
|
+
raise CertApiException(
|
|
77
|
+
"Could not determine Cloudflare registered domain",
|
|
78
|
+
detail={"domain": domain},
|
|
79
|
+
step="Cloudflare.determine_registered_domain"
|
|
80
|
+
)
|
|
68
81
|
|
|
69
82
|
def list_txt_records(self, domain: str, name_filter: str = None) -> list:
|
|
70
83
|
"""
|
|
@@ -81,12 +94,20 @@ class Cloudflare(object):
|
|
|
81
94
|
response = urlopen(Request(api_url, headers=request_headers))
|
|
82
95
|
|
|
83
96
|
if response.getcode() != 200:
|
|
84
|
-
raise
|
|
97
|
+
raise CertApiException(
|
|
98
|
+
"Cloudflare API error",
|
|
99
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
100
|
+
step="Cloudflare.list_txt_records"
|
|
101
|
+
)
|
|
85
102
|
|
|
86
103
|
result = json.loads(response.read().decode("utf8"))
|
|
87
104
|
if not result.get("success"):
|
|
88
105
|
print(f"List TXT record [{response.getcode()}]", result)
|
|
89
|
-
raise
|
|
106
|
+
raise CertApiException(
|
|
107
|
+
"Unknown error listing TXT records",
|
|
108
|
+
detail=result.get("errors", "Unknown error listing TXT records"),
|
|
109
|
+
step="Cloudflare.list_txt_records"
|
|
110
|
+
)
|
|
90
111
|
|
|
91
112
|
return result["result"]
|
|
92
113
|
|
|
@@ -115,7 +136,11 @@ class Cloudflare(object):
|
|
|
115
136
|
result = response.read().decode("utf8")
|
|
116
137
|
if response.getcode() != 200:
|
|
117
138
|
print(f"Create TXT record [{response.getcode()}]", result)
|
|
118
|
-
raise
|
|
139
|
+
raise CertApiException(
|
|
140
|
+
"Cloudflare API error",
|
|
141
|
+
detail=json.loads(result),
|
|
142
|
+
step="Cloudflare.create_record"
|
|
143
|
+
)
|
|
119
144
|
|
|
120
145
|
return json.loads(result)["result"]["id"]
|
|
121
146
|
|
|
@@ -136,4 +161,8 @@ class Cloudflare(object):
|
|
|
136
161
|
result = response.read().decode("utf8")
|
|
137
162
|
if response.getcode() != 200:
|
|
138
163
|
print(f"Delete dns record [{response.getcode()}]", result)
|
|
139
|
-
raise
|
|
164
|
+
raise CertApiException(
|
|
165
|
+
"Cloudflare API error",
|
|
166
|
+
detail=json.loads(result),
|
|
167
|
+
step="Cloudflare.delete_record"
|
|
168
|
+
)
|
|
@@ -3,6 +3,7 @@ from collections.abc import MutableMapping
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
from ...ChallengeSolver import ChallengeSolver
|
|
5
5
|
from .digitalocean_client import DigitalOcean
|
|
6
|
+
from certapi.errors import CertApiException
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class DigitalOceanChallengeSolver(ChallengeSolver):
|
|
@@ -21,7 +22,13 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
|
|
|
21
22
|
try:
|
|
22
23
|
self.digitalocean.determine_domain(domain)
|
|
23
24
|
return True
|
|
24
|
-
except
|
|
25
|
+
except CertApiException as e:
|
|
26
|
+
# Log the exception or handle it as needed, but return False as it doesn't support the domain
|
|
27
|
+
print(f"DigitalOceanChallengeSolver.supports_domain: {e.message} - {e.detail}")
|
|
28
|
+
return False
|
|
29
|
+
except Exception as e:
|
|
30
|
+
# Catch any other unexpected exceptions
|
|
31
|
+
print(f"DigitalOceanChallengeSolver.supports_domain: An unexpected error occurred: {e}")
|
|
25
32
|
return False
|
|
26
33
|
|
|
27
34
|
def save_challenge(self, key: str, value: str, domain=None):
|
|
@@ -31,7 +38,7 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
|
|
|
31
38
|
|
|
32
39
|
record_id = self.digitalocean.create_record(name=key, data=value, domain=base_domain)
|
|
33
40
|
self.challenges_map[key] = record_id
|
|
34
|
-
print(f"
|
|
41
|
+
print(f"DigitalOceanChallengeSolver: Saved challenge for {key} with record ID {record_id}")
|
|
35
42
|
|
|
36
43
|
def get_challenge(self, key: str, domain: str) -> str:
|
|
37
44
|
base_domain = self.digitalocean.determine_domain(domain)
|
|
@@ -39,7 +46,7 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
|
|
|
39
46
|
for record in records:
|
|
40
47
|
if record["name"] == key:
|
|
41
48
|
return record["data"] # DigitalOcean API returns 'data' for TXT record content
|
|
42
|
-
return None # Return None if not found, as per
|
|
49
|
+
return None # Return None if not found, as per ChallengeSolver's __getitem__ behavior
|
|
43
50
|
|
|
44
51
|
def delete_challenge(self, key: str, domain: str):
|
|
45
52
|
if key not in self.challenges_map:
|
|
@@ -49,7 +56,27 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
|
|
|
49
56
|
base_domain = self.digitalocean.determine_domain(domain)
|
|
50
57
|
self.digitalocean.delete_record(record=record_id, domain=base_domain)
|
|
51
58
|
del self.challenges_map[key]
|
|
52
|
-
print(f"
|
|
59
|
+
print(f"DigitalOceanChallengeSolver: Deleted challenge for {key} with record ID {record_id}")
|
|
60
|
+
|
|
61
|
+
def cleanup_old_challenges(self):
|
|
62
|
+
domains = self.digitalocean._get_domains()
|
|
63
|
+
for domain_obj in domains:
|
|
64
|
+
domain_name = domain_obj["name"]
|
|
65
|
+
try:
|
|
66
|
+
records = self.digitalocean.list_records(domain_name)
|
|
67
|
+
for record in records:
|
|
68
|
+
if record["type"] == "TXT" and record["name"].startswith("_acme-challenge"):
|
|
69
|
+
print(f"DigitalOceanChallengeSolver: Deleting old challenge record {record['name']} in domain {domain_name}")
|
|
70
|
+
try:
|
|
71
|
+
self.digitalocean.delete_record(record["id"], domain_name)
|
|
72
|
+
except CertApiException as e:
|
|
73
|
+
print(f"DigitalOceanChallengeSolver: Warning - Failed to delete record {record['name']} in domain {domain_name}: {e.message} - {e.detail}")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"DigitalOceanChallengeSolver: Warning - An unexpected error occurred while deleting record {record['name']} in domain {domain_name}: {e}")
|
|
76
|
+
except CertApiException as e:
|
|
77
|
+
print(f"DigitalOceanChallengeSolver: Error listing challenges in domain {domain_name}: {e.message} - {e.detail}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"DigitalOceanChallengeSolver: An unexpected error occurred while listing challenges in domain {domain_name}: {e}")
|
|
53
80
|
|
|
54
81
|
def __iter__(self):
|
|
55
82
|
# This is tricky as we can't easily iterate all challenges across all domains
|
{certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from os import getenv
|
|
3
|
-
|
|
4
|
-
from
|
|
3
|
+
from urllib.request import urlopen, Request
|
|
4
|
+
from certapi.errors import CertApiException
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class DigitalOcean(object):
|
|
@@ -11,18 +11,31 @@ class DigitalOcean(object):
|
|
|
11
11
|
if not self.token:
|
|
12
12
|
self.token = getenv("DIGITALOCEAN_API_KEY")
|
|
13
13
|
if not self.token:
|
|
14
|
-
raise
|
|
14
|
+
raise CertApiException("DIGITALOCEAN_API_KEY not found in environment", step="DigitalOcean.__init__")
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
"""
|
|
16
|
+
def _get_domains(self):
|
|
17
|
+
"""Fetch DigitalOcean domains"""
|
|
18
18
|
request_headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(self.token)}
|
|
19
19
|
response = urlopen(Request(self.api, headers=request_headers))
|
|
20
20
|
if response.getcode() != 200:
|
|
21
|
-
raise
|
|
22
|
-
|
|
21
|
+
raise CertApiException(
|
|
22
|
+
"DigitalOcean API error",
|
|
23
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
24
|
+
step="DigitalOcean._get_domains"
|
|
25
|
+
)
|
|
26
|
+
return json.loads(response.read().decode("utf8"))["domains"]
|
|
27
|
+
|
|
28
|
+
def determine_domain(self, domain):
|
|
29
|
+
"""Determine registered domain in API"""
|
|
30
|
+
domains = self._get_domains()
|
|
23
31
|
for d in domains:
|
|
24
32
|
if d["name"] in domain:
|
|
25
33
|
return d["name"]
|
|
34
|
+
raise CertApiException(
|
|
35
|
+
"No DigitalOcean domain found for: {0}".format(domain),
|
|
36
|
+
detail={"domain": domain},
|
|
37
|
+
step="DigitalOcean.determine_domain"
|
|
38
|
+
)
|
|
26
39
|
|
|
27
40
|
def create_record(self, name, data, domain):
|
|
28
41
|
"""
|
|
@@ -40,7 +53,11 @@ class DigitalOcean(object):
|
|
|
40
53
|
request_data = {"type": "TXT", "ttl": 300, "name": name, "data": data}
|
|
41
54
|
response = urlopen(Request(api, data=json.dumps(request_data).encode("utf8"), headers=request_headers))
|
|
42
55
|
if response.getcode() != 201:
|
|
43
|
-
raise
|
|
56
|
+
raise CertApiException(
|
|
57
|
+
"DigitalOcean API error",
|
|
58
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
59
|
+
step="DigitalOcean.create_record"
|
|
60
|
+
)
|
|
44
61
|
return json.loads(response.read().decode("utf8"))["domain_record"]["id"]
|
|
45
62
|
|
|
46
63
|
def delete_record(self, record, domain):
|
|
@@ -57,7 +74,11 @@ class DigitalOcean(object):
|
|
|
57
74
|
request.get_method = lambda: "DELETE"
|
|
58
75
|
response = urlopen(request)
|
|
59
76
|
if response.getcode() != 204:
|
|
60
|
-
raise
|
|
77
|
+
raise CertApiException(
|
|
78
|
+
"DigitalOcean API error",
|
|
79
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
80
|
+
step="DigitalOcean.delete_record"
|
|
81
|
+
)
|
|
61
82
|
|
|
62
83
|
def list_records(self, domain, name_filter=None):
|
|
63
84
|
"""
|
|
@@ -73,7 +94,11 @@ class DigitalOcean(object):
|
|
|
73
94
|
request_headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(self.token)}
|
|
74
95
|
response = urlopen(Request(api, headers=request_headers))
|
|
75
96
|
if response.getcode() != 200:
|
|
76
|
-
raise
|
|
97
|
+
raise CertApiException(
|
|
98
|
+
"DigitalOcean API error",
|
|
99
|
+
detail=json.loads(response.read().decode("utf8")),
|
|
100
|
+
step="DigitalOcean.list_records"
|
|
101
|
+
)
|
|
77
102
|
|
|
78
103
|
all_records = json.loads(response.read().decode("utf8"))["domain_records"]
|
|
79
104
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CertApiException(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Base exception for all CertAPI related errors.
|
|
4
|
+
"""
|
|
5
|
+
def __init__(self, message: str, detail: dict = None, step: str = None):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.message = message
|
|
8
|
+
self.detail = detail if detail is not None else {}
|
|
9
|
+
self.step = step
|
|
10
|
+
self.can_retry = False
|
|
11
|
+
|
|
12
|
+
def json_obj(self) -> dict:
|
|
13
|
+
return {
|
|
14
|
+
"message": self.message,
|
|
15
|
+
"step": self.step,
|
|
16
|
+
"detail": self.detail
|
|
17
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|