devnomads 0.3.0__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.
- {devnomads-0.3.0 → devnomads-0.4.0}/PKG-INFO +1 -1
- {devnomads-0.3.0 → devnomads-0.4.0}/openapi.json +140 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/pyproject.toml +1 -1
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/client.py +19 -3
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/api/_services.py +37 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_acme_client.py +23 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/uv.lock +1213 -1203
- {devnomads-0.3.0 → devnomads-0.4.0}/.flake8 +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/.gitignore +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/.gitlab-ci.yml +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/Makefile +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/README.md +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/__init__.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/__init__.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/challenge_server.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/dns01.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/errors.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/http01.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/keys.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/acme/verify.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/api/__init__.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/api/client.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/api/credentials.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/api/errors.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/dns/__init__.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/dns/errors.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/dns/names.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/dns/zones.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/src/devnomads/py.typed +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/conftest.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_acme_challenges.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_acme_keys.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_client.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_credentials.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_dns.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_names.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tests/test_services.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tools/bump_version.py +0 -0
- {devnomads-0.3.0 → devnomads-0.4.0}/tools/generate.py +0 -0
|
@@ -2926,6 +2926,146 @@
|
|
|
2926
2926
|
]
|
|
2927
2927
|
}
|
|
2928
2928
|
},
|
|
2929
|
+
"/services/servers/{serviceId}/snapshots": {
|
|
2930
|
+
"get": {
|
|
2931
|
+
"tags": [
|
|
2932
|
+
"Server"
|
|
2933
|
+
],
|
|
2934
|
+
"summary": "Get list of server snapshots",
|
|
2935
|
+
"description": "Get list of snapshots for a server service",
|
|
2936
|
+
"operationId": "services.servers.snapshots.index",
|
|
2937
|
+
"parameters": [
|
|
2938
|
+
{
|
|
2939
|
+
"name": "serviceId",
|
|
2940
|
+
"in": "path",
|
|
2941
|
+
"description": "Service ID",
|
|
2942
|
+
"required": true,
|
|
2943
|
+
"schema": {
|
|
2944
|
+
"type": "integer"
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
],
|
|
2948
|
+
"responses": {
|
|
2949
|
+
"200": {
|
|
2950
|
+
"description": "Success"
|
|
2951
|
+
}
|
|
2952
|
+
},
|
|
2953
|
+
"security": [
|
|
2954
|
+
{
|
|
2955
|
+
"bearerToken": []
|
|
2956
|
+
}
|
|
2957
|
+
]
|
|
2958
|
+
},
|
|
2959
|
+
"post": {
|
|
2960
|
+
"tags": [
|
|
2961
|
+
"Server"
|
|
2962
|
+
],
|
|
2963
|
+
"summary": "Create server snapshot",
|
|
2964
|
+
"description": "Create a new snapshot for a server service",
|
|
2965
|
+
"operationId": "services.servers.snapshots.create",
|
|
2966
|
+
"parameters": [
|
|
2967
|
+
{
|
|
2968
|
+
"name": "serviceId",
|
|
2969
|
+
"in": "path",
|
|
2970
|
+
"description": "Service ID",
|
|
2971
|
+
"required": true,
|
|
2972
|
+
"schema": {
|
|
2973
|
+
"type": "integer"
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
],
|
|
2977
|
+
"responses": {
|
|
2978
|
+
"202": {
|
|
2979
|
+
"description": "Accepted"
|
|
2980
|
+
}
|
|
2981
|
+
},
|
|
2982
|
+
"security": [
|
|
2983
|
+
{
|
|
2984
|
+
"bearerToken": []
|
|
2985
|
+
}
|
|
2986
|
+
]
|
|
2987
|
+
}
|
|
2988
|
+
},
|
|
2989
|
+
"/services/servers/{serviceId}/snapshots/{snapshotUuid}/restore": {
|
|
2990
|
+
"get": {
|
|
2991
|
+
"tags": [
|
|
2992
|
+
"Server"
|
|
2993
|
+
],
|
|
2994
|
+
"summary": "Restore server snapshot",
|
|
2995
|
+
"description": "Restore a snapshot for a server service",
|
|
2996
|
+
"operationId": "services.servers.snapshots.restore",
|
|
2997
|
+
"parameters": [
|
|
2998
|
+
{
|
|
2999
|
+
"name": "serviceId",
|
|
3000
|
+
"in": "path",
|
|
3001
|
+
"description": "Service ID",
|
|
3002
|
+
"required": true,
|
|
3003
|
+
"schema": {
|
|
3004
|
+
"type": "integer"
|
|
3005
|
+
}
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
"name": "snapshotUuid",
|
|
3009
|
+
"in": "path",
|
|
3010
|
+
"description": "Snapshot UUID",
|
|
3011
|
+
"required": true,
|
|
3012
|
+
"schema": {
|
|
3013
|
+
"type": "string"
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
],
|
|
3017
|
+
"responses": {
|
|
3018
|
+
"202": {
|
|
3019
|
+
"description": "Accepted"
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
"security": [
|
|
3023
|
+
{
|
|
3024
|
+
"bearerToken": []
|
|
3025
|
+
}
|
|
3026
|
+
]
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
"/services/servers/{serviceId}/snapshots/{snapshotUuid}": {
|
|
3030
|
+
"delete": {
|
|
3031
|
+
"tags": [
|
|
3032
|
+
"Server"
|
|
3033
|
+
],
|
|
3034
|
+
"summary": "Delete server snapshot",
|
|
3035
|
+
"description": "Delete a snapshot for a server service",
|
|
3036
|
+
"operationId": "services.servers.snapshots.delete",
|
|
3037
|
+
"parameters": [
|
|
3038
|
+
{
|
|
3039
|
+
"name": "serviceId",
|
|
3040
|
+
"in": "path",
|
|
3041
|
+
"description": "Service ID",
|
|
3042
|
+
"required": true,
|
|
3043
|
+
"schema": {
|
|
3044
|
+
"type": "integer"
|
|
3045
|
+
}
|
|
3046
|
+
},
|
|
3047
|
+
{
|
|
3048
|
+
"name": "snapshotUuid",
|
|
3049
|
+
"in": "path",
|
|
3050
|
+
"description": "Snapshot UUID",
|
|
3051
|
+
"required": true,
|
|
3052
|
+
"schema": {
|
|
3053
|
+
"type": "string"
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
],
|
|
3057
|
+
"responses": {
|
|
3058
|
+
"202": {
|
|
3059
|
+
"description": "Accepted"
|
|
3060
|
+
}
|
|
3061
|
+
},
|
|
3062
|
+
"security": [
|
|
3063
|
+
{
|
|
3064
|
+
"bearerToken": []
|
|
3065
|
+
}
|
|
3066
|
+
]
|
|
3067
|
+
}
|
|
3068
|
+
},
|
|
2929
3069
|
"/services": {
|
|
2930
3070
|
"get": {
|
|
2931
3071
|
"tags": [
|
|
@@ -88,6 +88,9 @@ def _challenge_for_authz(challenge_types_map: dict[str, str], authz_body) -> str
|
|
|
88
88
|
ident = getattr(getattr(authz_body, "identifier", None), "value", None)
|
|
89
89
|
if ident is None:
|
|
90
90
|
return None
|
|
91
|
+
# The map is keyed by lowercased identifiers; lowercase the authz value
|
|
92
|
+
# too (belt and braces — CAs already lowercase it) so the lookup matches.
|
|
93
|
+
ident = ident.lower()
|
|
91
94
|
lookup = f"*.{ident}" if getattr(authz_body, "wildcard", False) else ident
|
|
92
95
|
return challenge_types_map.get(lookup)
|
|
93
96
|
|
|
@@ -415,7 +418,17 @@ class AcmeClient:
|
|
|
415
418
|
|
|
416
419
|
|
|
417
420
|
def _normalize_identifiers(names: list[str]) -> list[str]:
|
|
418
|
-
"""IDNA-encode and de-duplicate identifiers, preserving order.
|
|
421
|
+
"""IDNA-encode, lowercase and de-duplicate identifiers, preserving order.
|
|
422
|
+
|
|
423
|
+
DNS names are case-insensitive and ACME servers (Let's Encrypt) echo
|
|
424
|
+
authorization identifiers back lowercased. Python's IDNA codec, however,
|
|
425
|
+
preserves the case of plain-ASCII labels, so a mixed-case config entry
|
|
426
|
+
like ``www.Example.com`` would otherwise key the challenge-type map in a
|
|
427
|
+
case the CA never uses — the challenge lookup at answer time then misses
|
|
428
|
+
and issuance fails with "no challenge type configured". Lowercasing here
|
|
429
|
+
keeps every identifier canonical (punycode output is already lowercase,
|
|
430
|
+
so this is a no-op for internationalised names).
|
|
431
|
+
"""
|
|
419
432
|
|
|
420
433
|
identifiers: list[str] = []
|
|
421
434
|
seen: set[str] = set()
|
|
@@ -423,7 +436,7 @@ def _normalize_identifiers(names: list[str]) -> list[str]:
|
|
|
423
436
|
if not name:
|
|
424
437
|
continue
|
|
425
438
|
try:
|
|
426
|
-
ascii_name = name.encode("idna").decode("ascii")
|
|
439
|
+
ascii_name = name.encode("idna").decode("ascii").lower()
|
|
427
440
|
except Exception: # noqa: BLE001 # nosec B112
|
|
428
441
|
continue
|
|
429
442
|
if ascii_name not in seen:
|
|
@@ -442,11 +455,14 @@ def _normalize_challenge_types(
|
|
|
442
455
|
|
|
443
456
|
result: dict[str, str] = {}
|
|
444
457
|
for ident in identifiers:
|
|
458
|
+
# ``identifiers`` are already IDNA-normalised and lowercased; resolve
|
|
459
|
+
# each against the caller's map case-insensitively so a mixed-case
|
|
460
|
+
# key (``www.Example.com``) still matches its lowercased identifier.
|
|
445
461
|
ct = challenge_types.get(ident)
|
|
446
462
|
if ct is None:
|
|
447
463
|
for key, value in challenge_types.items():
|
|
448
464
|
try:
|
|
449
|
-
if key.encode("idna").decode("ascii") == ident:
|
|
465
|
+
if key.encode("idna").decode("ascii").lower() == ident:
|
|
450
466
|
ct = value
|
|
451
467
|
break
|
|
452
468
|
except Exception: # noqa: BLE001 # nosec B112
|
|
@@ -717,6 +717,43 @@ class ServersResource:
|
|
|
717
717
|
path = f"/services/servers/{service_id}/state/{state}"
|
|
718
718
|
return self._client.request("GET", path, params=params)
|
|
719
719
|
|
|
720
|
+
@property
|
|
721
|
+
def snapshots(self) -> ServersSnapshotsResource:
|
|
722
|
+
return ServersSnapshotsResource(self._client)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class ServersSnapshotsResource:
|
|
726
|
+
"""Generated resource accessor."""
|
|
727
|
+
|
|
728
|
+
def __init__(self, client: Client) -> None:
|
|
729
|
+
self._client = client
|
|
730
|
+
|
|
731
|
+
def create(
|
|
732
|
+
self, service_id: str, params: dict | None = None, body: Any = None
|
|
733
|
+
) -> Any:
|
|
734
|
+
"""Create server snapshot"""
|
|
735
|
+
path = f"/services/servers/{service_id}/snapshots"
|
|
736
|
+
return self._client.request("POST", path, params=params, json_body=body)
|
|
737
|
+
|
|
738
|
+
def delete(
|
|
739
|
+
self, service_id: str, snapshot_uuid: str, params: dict | None = None
|
|
740
|
+
) -> Any:
|
|
741
|
+
"""Delete server snapshot"""
|
|
742
|
+
path = f"/services/servers/{service_id}/snapshots/{snapshot_uuid}"
|
|
743
|
+
return self._client.request("DELETE", path, params=params)
|
|
744
|
+
|
|
745
|
+
def list(self, service_id: str, params: dict | None = None) -> Any:
|
|
746
|
+
"""Get list of server snapshots"""
|
|
747
|
+
path = f"/services/servers/{service_id}/snapshots"
|
|
748
|
+
return self._client.request("GET", path, params=params)
|
|
749
|
+
|
|
750
|
+
def restore(
|
|
751
|
+
self, service_id: str, snapshot_uuid: str, params: dict | None = None
|
|
752
|
+
) -> Any:
|
|
753
|
+
"""Restore server snapshot"""
|
|
754
|
+
path = f"/services/servers/{service_id}/snapshots/{snapshot_uuid}/restore"
|
|
755
|
+
return self._client.request("GET", path, params=params)
|
|
756
|
+
|
|
720
757
|
|
|
721
758
|
class SitesResource:
|
|
722
759
|
"""Generated resource accessor."""
|
|
@@ -51,6 +51,29 @@ def test_challenge_for_authz_wildcard_reprefixes():
|
|
|
51
51
|
assert _challenge_for_authz({"*.example.com": "dns-01"}, body) == "dns-01"
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def test_normalize_identifiers_lowercases():
|
|
55
|
+
# DNS is case-insensitive and CAs lowercase identifiers; a mixed-case
|
|
56
|
+
# ASCII label must not survive with its original case (Python's IDNA
|
|
57
|
+
# codec would otherwise preserve it).
|
|
58
|
+
assert _normalize_identifiers(["www.Ahlersbuilding.BE"]) == [
|
|
59
|
+
"www.ahlersbuilding.be"
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_mixed_case_challenge_map_resolves_against_lowercased_authz():
|
|
64
|
+
"""Regression: a mixed-case config entry keyed the challenge-type map in
|
|
65
|
+
a case the CA never uses, so the challenge lookup missed and issuance
|
|
66
|
+
failed with "no challenge type configured". The map must resolve against
|
|
67
|
+
the lowercased authorization identifier the CA echoes back."""
|
|
68
|
+
# certmonger builds the map keyed by the original (mixed) case:
|
|
69
|
+
challenge_types = {"www.Ahlersbuilding.be": "dns-01"}
|
|
70
|
+
identifiers = _normalize_identifiers(["www.Ahlersbuilding.be"])
|
|
71
|
+
types_map = _normalize_challenge_types(challenge_types, identifiers)
|
|
72
|
+
# LE hands back the identifier lowercased:
|
|
73
|
+
body = _authz("www.ahlersbuilding.be", False)
|
|
74
|
+
assert _challenge_for_authz(types_map, body) == "dns-01"
|
|
75
|
+
|
|
76
|
+
|
|
54
77
|
def test_split_fullchain():
|
|
55
78
|
leaf = "-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n"
|
|
56
79
|
rest = "-----BEGIN CERTIFICATE-----\nBBB\n-----END CERTIFICATE-----\n"
|