certapi 0.3.1__tar.gz → 0.4.0__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.
Files changed (55) hide show
  1. {certapi-0.3.1 → certapi-0.4.0}/PKG-INFO +1 -1
  2. {certapi-0.3.1 → certapi-0.4.0}/pyproject.toml +1 -1
  3. {certapi-0.3.1 → certapi-0.4.0}/setup.py +1 -1
  4. certapi-0.4.0/src/certapi/__init__.py +9 -0
  5. certapi-0.4.0/src/certapi/acme/Acme.py +123 -0
  6. certapi-0.4.0/src/certapi/acme/AcmeError.py +141 -0
  7. certapi-0.4.0/src/certapi/acme/Challenge.py +90 -0
  8. certapi-0.4.0/src/certapi/acme/Order.py +49 -0
  9. certapi-0.4.0/src/certapi/acme/__init__.py +4 -0
  10. certapi-0.4.0/src/certapi/acme/http.py +56 -0
  11. certapi-0.4.0/src/certapi/challenge_store/ChallengeStore.py +34 -0
  12. certapi-0.4.0/src/certapi/challenge_store/FileSystemChallengeStore.py +38 -0
  13. certapi-0.4.0/src/certapi/challenge_store/FilesystemChallengeStore.py +38 -0
  14. certapi-0.4.0/src/certapi/challenge_store/InmemoryChallengeStore.py +26 -0
  15. certapi-0.4.0/src/certapi/challenge_store/__init__.py +25 -0
  16. certapi-0.4.0/src/certapi/challenge_store/dns/__init__.py +2 -0
  17. certapi-0.4.0/src/certapi/client/__init__.py +0 -0
  18. certapi-0.4.0/src/certapi/client/cert_manager_client.py +98 -0
  19. certapi-0.4.0/src/certapi/crypto/__init__.py +2 -0
  20. {certapi-0.3.1/src/certapi → certapi-0.4.0/src/certapi/crypto}/crypto.py +31 -78
  21. {certapi-0.3.1/src/certapi → certapi-0.4.0/src/certapi/crypto}/crypto_classes.py +26 -14
  22. certapi-0.4.0/src/certapi/issuers/AcmeCertIssuer.py +145 -0
  23. certapi-0.3.1/src/certapi/custom_certauthority.py → certapi-0.4.0/src/certapi/issuers/SelfCertIssuer.py +10 -50
  24. certapi-0.4.0/src/certapi/issuers/__init__.py +3 -0
  25. certapi-0.4.0/src/certapi/issuers/abstract_certissuer.py +95 -0
  26. certapi-0.4.0/src/certapi/keystore/FileSystemKeystore.py +99 -0
  27. certapi-0.4.0/src/certapi/keystore/KeyStore.py +66 -0
  28. certapi-0.4.0/src/certapi/keystore/PostgresqlKeyStore.py +229 -0
  29. certapi-0.4.0/src/certapi/keystore/RemoteKeyStore.py +52 -0
  30. certapi-0.4.0/src/certapi/keystore/SqliteKeyStore.py +200 -0
  31. certapi-0.4.0/src/certapi/keystore/__init__.py +4 -0
  32. certapi-0.4.0/src/certapi/server/__init__.py +0 -0
  33. certapi-0.4.0/src/certapi/server/api.py +89 -0
  34. certapi-0.4.0/src/certapi/server/cert_api.py +110 -0
  35. certapi-0.4.0/src/certapi/server/key_api.py +79 -0
  36. certapi-0.4.0/src/certapi/utils.py +34 -0
  37. {certapi-0.3.1 → certapi-0.4.0}/src/certapi.egg-info/PKG-INFO +1 -1
  38. certapi-0.4.0/src/certapi.egg-info/SOURCES.txt +43 -0
  39. certapi-0.3.1/src/certapi/Acme.py +0 -426
  40. certapi-0.3.1/src/certapi/__init__.py +0 -6
  41. certapi-0.3.1/src/certapi/certauthority.py +0 -224
  42. certapi-0.3.1/src/certapi/challenge.py +0 -103
  43. certapi-0.3.1/src/certapi/cloudflare_challenge_store.py +0 -57
  44. certapi-0.3.1/src/certapi/cloudflare_client.py +0 -137
  45. certapi-0.3.1/src/certapi/db.py +0 -408
  46. certapi-0.3.1/src/certapi/digitalocean_challenge_store.py +0 -57
  47. certapi-0.3.1/src/certapi/digitalocean_client.py +0 -85
  48. certapi-0.3.1/src/certapi.egg-info/SOURCES.txt +0 -22
  49. {certapi-0.3.1 → certapi-0.4.0}/MANIFEST.in +0 -0
  50. {certapi-0.3.1 → certapi-0.4.0}/README.md +0 -0
  51. {certapi-0.3.1 → certapi-0.4.0}/setup.cfg +0 -0
  52. {certapi-0.3.1 → certapi-0.4.0}/src/certapi/util.py +0 -0
  53. {certapi-0.3.1 → certapi-0.4.0}/src/certapi.egg-info/dependency_links.txt +0 -0
  54. {certapi-0.3.1 → certapi-0.4.0}/src/certapi.egg-info/requires.txt +0 -0
  55. {certapi-0.3.1 → certapi-0.4.0}/src/certapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Python Package for managing keys, request SSL certificates from ACME.
5
5
  Home-page: https://github.com/mesudip/certapi
6
6
  Author: Sudip Bhattarai
@@ -2,7 +2,6 @@
2
2
  line-length = 120
3
3
 
4
4
  [tool.pytest.ini_options]
5
-
6
5
  minversion = "6.0"
7
6
  addopts = "-ra -q"
8
7
  testpaths = [
@@ -10,6 +9,7 @@ testpaths = [
10
9
  ]
11
10
  env_files =[".env", ".test.env"]
12
11
  pythonpath = "src"
12
+ timeout = 10
13
13
 
14
14
  [build-system]
15
15
  requires = ["setuptools>=42"]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.3.1",
5
+ version="0.4.0",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -0,0 +1,9 @@
1
+ from .acme.Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
2
+ from .manager.acme_cert_manager import AcmeCertManager
3
+ from .http.types import CertificateResponse, IssuedCert
4
+
5
+ from .crypto import Certificate,CertificateSigningRequest,CertificateSigningRequestBuilder \
6
+ ,Key,Ed25519Key,ECDSAKey,Ed25519PrivateKey,EllipticCurvePrivateKey
7
+ from .keystore import FileSystemKeystore,SqliteKeyStore,PostgresKeyStore,KeyStore
8
+ from .challenge_store import ChallengeStore, InMemoryChallengeStore, FileSystemChallengeStore,CloudflareChallengeStore,DigitalOceanChallengeStore
9
+ from .issuers import SelfCertIssuer
@@ -0,0 +1,123 @@
1
+ import os
2
+ import re
3
+ import threading
4
+ import time
5
+ from typing import Union, List, Tuple
6
+ import json
7
+ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
8
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
9
+ from cryptography.x509 import CertificateSigningRequest, Certificate
10
+ from certapi.crypto import crypto
11
+ import requests
12
+ from certapi.crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name, sign_for_jws,Key
13
+ from certapi.util import b64_encode, b64_string
14
+ from .AcmeError import *
15
+ from .http import *
16
+ from .Challenge import Challenge
17
+ from .Order import Order
18
+
19
+ class Acme:
20
+
21
+ URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
22
+ URL_PROD = "https://acme-v02.api.letsencrypt.org/directory"
23
+
24
+ def __init__(self, account_key: Union[RSAPrivateKey, EllipticCurvePrivateKey | Key], url=URL_STAGING):
25
+ self.account_key = account_key.key if isinstance(account_key, Key) else account_key
26
+
27
+ # json web key format for public key
28
+ self.jwk = jwk(self.account_key)
29
+ print(self.jwk)
30
+ self.nonce = []
31
+ self.acme_url = url if url else os.environ.get("ACME_API_URL", self.URL_STAGING)
32
+ self.key_id = None
33
+ self.directory = None
34
+ self._nonce_lock = threading.Lock() # Mutex for safe access to nonce
35
+
36
+ def setup(self):
37
+ if self.directory is None:
38
+ self.directory = get("Fetch Acme Directory", self.acme_url).json()
39
+
40
+ def _directory(self, key):
41
+ if not self.directory:
42
+ self.directory = get("Fetch Acme Directory", self.acme_url).json()
43
+ return self.directory[key]
44
+
45
+ def _directory_req(self, path_name, payload, depth=0):
46
+ url = self._directory(path_name)
47
+ return self._signed_req(url, payload, depth, step="Acme request:" + path_name)
48
+
49
+ def get_nonce(self, step: str, counter=1):
50
+ nonce = None
51
+ with self._nonce_lock: # Acquire nonce
52
+ if self.nonce:
53
+ nonce = self.nonce.pop(0)
54
+ return (
55
+ nonce
56
+ if nonce
57
+ else get(
58
+ step + " > Fetch new Nonce" if step else "Fetch new Nonce from Acme", self._directory("newNonce")
59
+ ).headers.get("Replay-Nonce")
60
+ )
61
+
62
+ def record_nonce(self, response: requests.Response) -> requests.Response:
63
+ with self._nonce_lock:
64
+ self.nonce.append(response.headers.get("Replay-Nonce", None))
65
+ return response
66
+
67
+ def _signed_req(
68
+ self, url, payload: Union[str, dict, list, bytes, None] = None, depth=0, step="Acme Request", throw=True
69
+ ) -> requests.Response:
70
+ payload64 = b64_encode(payload) if payload is not None else b""
71
+
72
+ protected = {
73
+ "url": url,
74
+ "alg": get_algorithm_name(self.account_key),
75
+ "nonce": self.get_nonce(step),
76
+ }
77
+
78
+ if self.key_id:
79
+ protected["kid"] = self.key_id
80
+ else:
81
+ protected["jwk"] = self.jwk
82
+ protectedb64 = b64_encode(protected)
83
+ payload = {
84
+ "protected": protectedb64.decode("utf-8"),
85
+ "payload": payload64.decode("utf-8"),
86
+ "signature": b64_string(sign_for_jws(self.account_key, b".".join([protectedb64, payload64]))),
87
+ }
88
+ try:
89
+
90
+ response = post(step, url, json=payload, headers={"Content-Type": "application/jose+json"}, throw=throw)
91
+ except AcmeError as e:
92
+ if e.can_retry and depth <= 0:
93
+ time.sleep(2)
94
+ return self._signed_req(url, payload, depth + 1, step, throw)
95
+ else:
96
+ raise e
97
+
98
+ return self.record_nonce(response)
99
+
100
+ def register(self):
101
+ response = self._directory_req("newAccount", {"termsOfServiceAgreed": True})
102
+ if "location" in response.headers:
103
+ self.key_id = response.headers["location"]
104
+ return response
105
+
106
+ def create_authorized_order(self, domains: List[str]) -> "Order":
107
+ payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]}
108
+ res = self._directory_req("newOrder", payload)
109
+ res_json = res.json()
110
+ challenges = []
111
+ for auth_url in res_json["authorizations"]:
112
+ auth_res = self._signed_req(auth_url, None, step="Authorize Created Order")
113
+ challenges.append(Challenge(auth_url, auth_res.json(), self))
114
+ return Order(res.headers["location"], res_json, challenges, self)
115
+
116
+ def authorize_order(self, auth_url):
117
+ return self._signed_req(auth_url, None, step="Authorize Created Order")
118
+
119
+ def verify_challenge(self, challenge_url):
120
+ return self._signed_req(challenge_url, {}, step="Verify Challenge")
121
+
122
+ def finalize_order(self):
123
+ pass
@@ -0,0 +1,141 @@
1
+ import requests
2
+
3
+
4
+ class AcmeError(Exception):
5
+ def __init__(self, message, detail, step):
6
+ super().__init__(step, message)
7
+ self.message: str = message
8
+ self.step: str = step
9
+ self.detail: dict = detail
10
+ self.can_retry = False
11
+
12
+ def json_obj(self) -> dict:
13
+ return {"message": self.message, "step": self.step, "detail": self.detail}
14
+
15
+
16
+ class AcmeNetworkError(AcmeError, requests.RequestException):
17
+ """
18
+ There was an error connecting/communicating with the AcmeServer.
19
+ """
20
+
21
+ def __init__(self, request: requests.Request, message, detail, step):
22
+ # Initialize both parent classes
23
+ requests.RequestException.__init__(self, request=request)
24
+ AcmeError.__init__(self, message, detail, step)
25
+ requests.RequestException.__init__(self, request=request) # Pass message to RequestException
26
+ self.can_retry = True
27
+
28
+
29
+ class AcmeHttpError(AcmeError, requests.HTTPError):
30
+ """
31
+ Acme Server replied with error status.
32
+ """
33
+
34
+ def __init__(self, response: requests.Response, step: str):
35
+ requests.HTTPError.__init__(self, response=response)
36
+ self.response = response
37
+ (message, detail) = self.extract_acme_response_error()
38
+ AcmeError.__init__(self, message, detail, step)
39
+
40
+ def extract_acme_response_error(self):
41
+ message = None
42
+ error = None
43
+ try:
44
+ res_json = self.response.json()
45
+ if res_json["status"] == "invalid":
46
+ if res_json["challenges"]:
47
+ failed_challenge: dict = [x for x in res_json["challenges"] if x["status"] == "invalid"][0]
48
+ error = failed_challenge["error"]
49
+ err_detail: str = error.get("detail")
50
+ validation_record: dict = failed_challenge.get("validationRecord")
51
+ error = {
52
+ "url": failed_challenge["url"],
53
+ }
54
+ if validation_record is None:
55
+ # Search for the pattern and extract the content
56
+ if err_detail.startswith("DNS problem: NXDOMAIN"):
57
+ error["dns"] = {"error": "DNS record doesn't exist"}
58
+ error["hostname"] = re.findall(r"looking up [A-Z]+ for ([\w.-]+)", err_detail)[0]
59
+ message = error["hostname"] + " doesn't have a valid DNS record"
60
+ elif err_detail:
61
+ error["dns"] = {"error": err_detail}
62
+ message = err_detail
63
+ else:
64
+ error["dns"] = {"error": error}
65
+ message = err_detail
66
+ else:
67
+ validation_record = validation_record[0]
68
+ error["hostname"] = validation_record["hostname"]
69
+ error["dns"] = {
70
+ "resolved": validation_record["addressesResolved"],
71
+ "used": validation_record["addressUsed"],
72
+ }
73
+ if error["type"] == "urn:ietf:params:acme:error:connection":
74
+ if "Timeout during connect" in err_detail:
75
+ error["connect"] = {"error": "Timeout"}
76
+ message = (
77
+ error["hostname"]
78
+ + "["
79
+ + validation_record["addressUsed"]
80
+ + ":"
81
+ + validation_record["port"]
82
+ + "] Connect Timeout (Maybe firewall reasons)"
83
+ )
84
+ elif err_detail.endswith("Connection refused"):
85
+ error["connect"] = {"error": "connection refused"}
86
+ message = (
87
+ error["hostname"]
88
+ + "["
89
+ + validation_record["addressUsed"]
90
+ + ":"
91
+ + validation_record["port"]
92
+ + "] Connection Refused (Is http server running?)"
93
+ )
94
+ elif err_detail:
95
+ message = err_detail
96
+ else:
97
+ message = error
98
+ elif err_detail:
99
+ pattern = r'Invalid response from .*?: "(.*)"'
100
+
101
+ match = re.search(pattern, err_detail)
102
+
103
+ if match:
104
+ error["response"] = (match.group(1) if match is not None else err_detail,)
105
+ error["status_code"] = (error["status"],)
106
+ message = (
107
+ error["hostname"]
108
+ + " Status="
109
+ + error["status"]
110
+ + ": Invalid response in challenge url"
111
+ )
112
+ else:
113
+ message = err_detail
114
+ else:
115
+ message = error
116
+
117
+ if message is None:
118
+ if res_json.get("detail"):
119
+ message = res_json["detail"]
120
+ else:
121
+ message = "Received status=" + str(self.response.status_code) + " from AMCE server"
122
+ if error is None:
123
+ error = res_json
124
+
125
+ return (message, error)
126
+
127
+ except requests.RequestException as e:
128
+ message = "Received status=" + str(self.response.status_code) + " from AMCE server"
129
+ error = {"url": self.response.request.url, "response": self.response.text}
130
+ return (message, error)
131
+
132
+
133
+ class AcmeInvaliOrderError(AcmeHttpError):
134
+ def __init__(self, response: requests.Response, step: str):
135
+ super().__init__(response, step)
136
+
137
+
138
+ class AcmeInvaliNonceError(AcmeHttpError):
139
+ def __init__(self, response: requests.Response, step: str):
140
+ super().__init__(response, step)
141
+ self.can_retry = True
@@ -0,0 +1,90 @@
1
+
2
+ import json
3
+ from typing import Literal, Union
4
+
5
+ import requests
6
+
7
+ from certapi.util import b64_encode, b64_string
8
+ from certapi.crypto import digest_sha256
9
+ from .AcmeError import *
10
+ from .http import post,get
11
+
12
+ class Challenge:
13
+ def __init__(self, auth_url, data, acme):
14
+ self._auth_url = auth_url
15
+ self._acme = acme
16
+ self._data = data
17
+ challenge = self.get_challenge()
18
+ self.token:str = challenge["token"]
19
+ self.verified = challenge["status"] == "valid"
20
+ self.domain = data["identifier"]["value"] # Add domain attribute
21
+
22
+ jwk_json = json.dumps(self._acme.jwk, sort_keys=True, separators=(",", ":"))
23
+ thumbprint = b64_encode(digest_sha256(jwk_json.encode("utf8")))
24
+ self.authorization_key = "{0}.{1}".format(self.token, thumbprint.decode("utf-8"))
25
+
26
+ self.url = "http://{0}/.well-known/acme-challenge/{1}".format(data["identifier"]["value"], self.token)
27
+
28
+ def as_key_value(self,type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
29
+ challenge=self.get_challenge(type)
30
+
31
+ if challenge["type"] == "dns-01":
32
+ key = f"_acme-challenge.{self.domain}"
33
+ value = b64_string(digest_sha256(self.authorization_key.encode("utf8")))
34
+ else:
35
+ key = self.token
36
+ value = self.authorization_key
37
+
38
+ return (key,value)
39
+
40
+
41
+ def verify(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None) -> bool:
42
+ if not self.verified:
43
+ response = self._acme._signed_req(
44
+ self.get_challenge(type)["url"], {}, step="Verify Challenge", throw=False
45
+ )
46
+ if response.status_code == 200 and response.json()["status"] == "valid":
47
+ self.verified = True
48
+ return True
49
+ return False
50
+ return True
51
+
52
+ def self_verify(self) -> Union[bool, requests.Response]:
53
+ identifier = self._data["identifier"]
54
+ if identifier["type"] == "dns":
55
+ res = get("Self Domain verification", self.url)
56
+ if res.status_code == 200 and res.content == self.token.encode():
57
+ return True
58
+ else:
59
+ return res
60
+ return False
61
+
62
+ def query_progress(self) -> bool:
63
+ if self.verified:
64
+ return True
65
+ else:
66
+ res = self._acme._signed_req(self._auth_url, step="Acme Challenge Verification")
67
+ res_json = res.json()
68
+ if res_json["status"] == "valid":
69
+ self.verified = True
70
+ return True
71
+ elif res_json["status"] == "invalid":
72
+ raise AcmeInvaliOrderError(res, "Acme Challenge Verification")
73
+ else:
74
+ return False
75
+
76
+ def get_challenge(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
77
+ challenges = self._data["challenges"]
78
+ def_key= type if type is not None else "http-01"
79
+ for method in challenges:
80
+ if method["type"] == def_key:
81
+ return method
82
+ if type is None:
83
+ return challenges[0]
84
+
85
+ ch_types = [x["type"] for x in self._data["challenges"]]
86
+ raise AcmeError(
87
+ f"'{type}' not found in challenges. available:{str(ch_types)}",
88
+ {"response": self._data["challenges"]},
89
+ "Acme Challenge Verification",
90
+ )
@@ -0,0 +1,49 @@
1
+ from typing import List, Tuple
2
+ from certapi.crypto import crypto,csr_to_der
3
+ from certapi.util import b64_string
4
+ from cryptography.x509 import CertificateSigningRequest,Certificate
5
+ from .Challenge import *
6
+ from .http import get
7
+
8
+ class Order:
9
+ def __init__(self, url, data, challenges, acme):
10
+ self.url = url
11
+ self._data = data
12
+ self.all_challenges = challenges
13
+ self._acme = acme
14
+ self.status = "pending"
15
+
16
+ def remaining_challenges(self) -> List["Challenge"]:
17
+ return [x for x in self.all_challenges if not x.verified]
18
+
19
+ def refresh(self):
20
+ response = get("Fetch order Status", self.url)
21
+ self._data = response.json()
22
+ self.status = self._data["status"]
23
+ return response
24
+
25
+ def get_certificate(self) -> str:
26
+ if self.status == "processing":
27
+ raise ValueError(
28
+ "Order is still in 'processing' state! Wait until the order is finalized, and call `Order.refresh()` to update the state"
29
+ )
30
+ elif self.status != "valid":
31
+ raise ValueError("Order not in 'valid' state! Complete challenge and call finalize()")
32
+
33
+ certificate_res = self._acme._signed_req(
34
+ self._data["certificate"], step="Get Certificate from Successful Order"
35
+ )
36
+ return certificate_res.text
37
+
38
+ def finalize(self, csr: CertificateSigningRequest):
39
+ """
40
+ Finalizes the order with the provided CSR.
41
+
42
+ :param csr: The Certificate Signing Request (CSR) to be used for finalizing the order.
43
+ """
44
+ finalized = self._acme._signed_req(
45
+ self._data["finalize"], {"csr": b64_string(csr_to_der(csr))}, step="Order Finalize"
46
+ )
47
+ finalized_json = finalized.json()
48
+ self._data = finalized_json
49
+ self.status = finalized_json["status"]
@@ -0,0 +1,4 @@
1
+ from .Challenge import Challenge
2
+ from .AcmeError import *
3
+ from .Acme import Acme
4
+ from .Order import Order
@@ -0,0 +1,56 @@
1
+
2
+ import requests
3
+ from .AcmeError import *
4
+
5
+ def request(method, step: str, url: str, json=None, headers=None, throw=True) -> requests.Response:
6
+ res = None
7
+ try:
8
+ res = requests.request(method, url, json=json, headers=headers, timeout=15)
9
+ print("Request [" + str(res.status_code) + "] : " + method + " " + url + " step=" + step)
10
+ except requests.HTTPError as e:
11
+ status = res.status_code if res else None
12
+ status = status if status else (e.response.status_code if e.response else None)
13
+ if status:
14
+ print("Request [" + str(status) + "] : " + method + " " + url + " step=" + step)
15
+ else:
16
+ print("Request : " + method + " " + url + " step=" + step)
17
+
18
+ raise e
19
+ except requests.RequestException as e:
20
+ print("Request : " + str(method) + " " + str(url) + " step=" + str(step))
21
+ raise AcmeNetworkError(
22
+ e.request,
23
+ f"Error communicating with ACME server",
24
+ {
25
+ "errorType": e.__class__.__name__,
26
+ "message": str(e),
27
+ "method": method,
28
+ "url": e.request.url if e.request else None,
29
+ },
30
+ step,
31
+ )
32
+ if 199 <= res.status_code > 299:
33
+ [print(x, y) for (x, y) in res.headers.items()]
34
+ print("Response:", res.text)
35
+ json_data = None
36
+ try:
37
+ json_data = res.json()
38
+ except requests.RequestException as e:
39
+ pass
40
+ if json_data and json_data.get("type"):
41
+ errorType = json_data["type"]
42
+ if errorType == "urn:ietf:params:acme:error:badNonce":
43
+ raise AcmeInvaliNonceError(res, step=step)
44
+
45
+ if throw:
46
+ raise AcmeHttpError(res, step=step)
47
+ return res
48
+
49
+
50
+ def post(step: str, url: str, json=None, headers=None, throw=True) -> requests.Response:
51
+ return request("POST", step, url, json=json, headers=headers, throw=throw)
52
+
53
+
54
+ def get(step: str, url) -> requests.Response:
55
+ return request("GET", step, url)
56
+
@@ -0,0 +1,34 @@
1
+ from abc import ABC, abstractmethod
2
+ import os
3
+ from collections.abc import MutableMapping
4
+
5
+
6
+ class ChallengeStore(ABC):
7
+ """
8
+ Abstract base class for a challenge store.
9
+ """
10
+ @abstractmethod
11
+ def supports_domain(self, domain: str) -> bool:
12
+ pass
13
+
14
+ @abstractmethod
15
+ def save_challenge(self, key: str, value: str, domain: str = None):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_challenge(self, key: str, domain: str = None) -> str:
20
+ pass
21
+
22
+ @abstractmethod
23
+ def delete_challenge(self, key: str, domain: str = None):
24
+ pass
25
+ def store_details():
26
+ return {
27
+ "name": "Abstract Challenge Store",
28
+ }
29
+
30
+ def __iter__(self):
31
+ raise NotImplementedError("Must implement `__iter__` method.")
32
+
33
+ def __len__(self):
34
+ raise NotImplementedError("Must implement `__len__` method.")
@@ -0,0 +1,38 @@
1
+
2
+ from .ChallengeStore import ChallengeStore
3
+ import os
4
+
5
+ class FileSystemChallengeStore(ChallengeStore):
6
+ """
7
+ Filesystem implementation of the ChallengeStore.
8
+ """
9
+
10
+ def __init__(self, directory: str):
11
+ self.directory = directory
12
+ os.makedirs(self.directory, exist_ok=True)
13
+
14
+ def supports_domain(self, domain: str) -> bool:
15
+ return True
16
+
17
+ def save_challenge(self, key: str, value: str, domain: str = None):
18
+ file_path = os.path.join(self.directory, key)
19
+ with open(file_path, "w") as file:
20
+ file.write(value)
21
+
22
+ def get_challenge(self, key: str, domain: str = None) -> str:
23
+ file_path = os.path.join(self.directory, key)
24
+ if not os.path.exists(file_path):
25
+ return None
26
+ with open(file_path, "r") as file:
27
+ return file.read()
28
+
29
+ def delete_challenge(self, key: str, domain: str = None):
30
+ file_path = os.path.join(self.directory, key)
31
+ if os.path.exists(file_path):
32
+ os.remove(file_path)
33
+
34
+ def __iter__(self):
35
+ return (f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f)))
36
+
37
+ def __len__(self):
38
+ return len([f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f))])
@@ -0,0 +1,38 @@
1
+
2
+ from .ChallengeStore import ChallengeStore
3
+ import os
4
+
5
+ class FileSystemChallengeStore(ChallengeStore):
6
+ """
7
+ Filesystem implementation of the ChallengeStore.
8
+ """
9
+
10
+ def __init__(self, directory: str):
11
+ self.directory = directory
12
+ os.makedirs(self.directory, exist_ok=True)
13
+
14
+ def supports_domain(self, domain: str) -> bool:
15
+ return True
16
+
17
+ def save_challenge(self, key: str, value: str, domain: str = None):
18
+ file_path = os.path.join(self.directory, key)
19
+ with open(file_path, "w") as file:
20
+ file.write(value)
21
+
22
+ def get_challenge(self, key: str, domain: str = None) -> str:
23
+ file_path = os.path.join(self.directory, key)
24
+ if not os.path.exists(file_path):
25
+ return None
26
+ with open(file_path, "r") as file:
27
+ return file.read()
28
+
29
+ def delete_challenge(self, key: str, domain: str = None):
30
+ file_path = os.path.join(self.directory, key)
31
+ if os.path.exists(file_path):
32
+ os.remove(file_path)
33
+
34
+ def __iter__(self):
35
+ return (f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f)))
36
+
37
+ def __len__(self):
38
+ return len([f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f))])
@@ -0,0 +1,26 @@
1
+ from .ChallengeStore import ChallengeStore
2
+
3
+
4
+ class InMemoryChallengeStore(ChallengeStore):
5
+ """
6
+ In-memory implementation of the ChallengeStore.
7
+ """
8
+
9
+ def __init__(self):
10
+ self.challenges = {}
11
+
12
+ def save_challenge(self, key: str, value: str, domain: str = None):
13
+ self.challenges[key] = value
14
+
15
+ def get_challenge(self, key: str, domain: str = None) -> str:
16
+ return self.challenges.get(key, "")
17
+
18
+ def delete_challenge(self, key: str, domain: str = None):
19
+ if key in self.challenges:
20
+ del self.challenges[key]
21
+
22
+ def __iter__(self):
23
+ return iter(self.challenges)
24
+
25
+ def __len__(self):
26
+ return len(self.challenges)
@@ -0,0 +1,25 @@
1
+ from .ChallengeStore import ChallengeStore
2
+ from .InmemoryChallengeStore import InMemoryChallengeStore
3
+ import os
4
+ from .FileSystemChallengeStore import FileSystemChallengeStore
5
+ from .dns import CloudflareChallengeStore,DigitalOceanChallengeStore
6
+
7
+ def get_challenge_store():
8
+ """
9
+ Factory function to determine the type of store based on environment variables.
10
+
11
+ Environment Variables:
12
+ - `CHALLENGE_STORE_TYPE`: Can be "memory" or "filesystem".
13
+ - `CHALLENGE_STORE_DIR`: Directory for filesystem-based store. Defaults to "./challenges".
14
+ """
15
+ store_type = os.getenv("CHALLENGE_STORE_TYPE", "filesystem").lower()
16
+ directory = os.getenv("CHALLENGE_STORE_DIR", "./challenges")
17
+
18
+ if store_type == "memory":
19
+ return InMemoryChallengeStore()
20
+ elif store_type == "filesystem":
21
+ return FileSystemChallengeStore(directory)
22
+ else:
23
+ raise ValueError(f"Unknown CHALLENGE_STORE_TYPE: {store_type}")
24
+
25
+