ostium-ctl 0.1.0__py3-none-any.whl
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.
- ostium/__init__.py +25 -0
- ostium/__main__.py +4 -0
- ostium/_version.py +24 -0
- ostium/acme/__init__.py +0 -0
- ostium/acme/client.py +202 -0
- ostium/api/__init__.py +0 -0
- ostium/api/app.py +529 -0
- ostium/api/deps.py +142 -0
- ostium/api/errors.py +45 -0
- ostium/api/ratelimit.py +30 -0
- ostium/api/routes_alerts.py +36 -0
- ostium/api/routes_audit.py +25 -0
- ostium/api/routes_auth.py +361 -0
- ostium/api/routes_backups.py +166 -0
- ostium/api/routes_certs.py +284 -0
- ostium/api/routes_events.py +65 -0
- ostium/api/routes_inbounds.py +254 -0
- ostium/api/routes_jobs.py +51 -0
- ostium/api/routes_logs.py +56 -0
- ostium/api/routes_profiles.py +281 -0
- ostium/api/routes_rules.py +316 -0
- ostium/api/routes_settings.py +319 -0
- ostium/api/routes_stats.py +172 -0
- ostium/api/routes_subscriptions.py +59 -0
- ostium/api/routes_system.py +516 -0
- ostium/api/routes_users.py +423 -0
- ostium/cli/__init__.py +0 -0
- ostium/cli/main.py +1399 -0
- ostium/config.py +381 -0
- ostium/core/__init__.py +0 -0
- ostium/core/app.py +231 -0
- ostium/db/__init__.py +0 -0
- ostium/db/base.py +34 -0
- ostium/db/models.py +261 -0
- ostium/db/session.py +86 -0
- ostium/domain/__init__.py +0 -0
- ostium/domain/endpoints.py +313 -0
- ostium/engine/__init__.py +0 -0
- ostium/engine/core_api.py +122 -0
- ostium/engine/manager.py +277 -0
- ostium/engine/singbox/__init__.py +0 -0
- ostium/engine/singbox/clash_api.py +71 -0
- ostium/engine/singbox/config.py +485 -0
- ostium/engine/singbox/supervisor.py +394 -0
- ostium/engine/singbox/v2ray_stats.py +79 -0
- ostium/engine/singbox/v2ray_stats_pb2.py +46 -0
- ostium/engine/singbox/v2ray_stats_pb2_grpc.py +199 -0
- ostium/engine/singbox/validate.py +259 -0
- ostium/py.typed +0 -0
- ostium/services/__init__.py +0 -0
- ostium/services/audit.py +136 -0
- ostium/services/auth.py +342 -0
- ostium/services/backup.py +284 -0
- ostium/services/certificates.py +293 -0
- ostium/services/crypto.py +26 -0
- ostium/services/events.py +70 -0
- ostium/services/jobs.py +195 -0
- ostium/services/profiles.py +359 -0
- ostium/services/rules.py +133 -0
- ostium/services/settings.py +33 -0
- ostium/services/stats.py +293 -0
- ostium/services/subscriptions.py +141 -0
- ostium/services/users.py +354 -0
- ostium/system/__init__.py +0 -0
- ostium/system/proxy.py +574 -0
- ostium/system/windows_service.py +94 -0
- ostium/util/__init__.py +0 -0
- ostium/util/ids.py +33 -0
- ostium/util/redact.py +58 -0
- ostium/util/security.py +200 -0
- ostium/util/time.py +39 -0
- ostium/web/__init__.py +0 -0
- ostium/web/dist/.gitkeep +1 -0
- ostium/web/dist/assets/AppLayout-CnHo_1IA.css +1 -0
- ostium/web/dist/assets/AppLayout-TkOzyZjP.js +124 -0
- ostium/web/dist/assets/AuditPage-DKc-SkEr.css +1 -0
- ostium/web/dist/assets/AuditPage-DRMlsiOS.js +1 -0
- ostium/web/dist/assets/CertificatesPage-BALPvxEc.css +1 -0
- ostium/web/dist/assets/CertificatesPage-PoBGQlnF.js +1 -0
- ostium/web/dist/assets/CheckboxGroup-DaQGiz7w.js +105 -0
- ostium/web/dist/assets/ConnectionsPage-4L_Y9VaF.js +1 -0
- ostium/web/dist/assets/ConnectionsPage-BGHvE-XG.css +1 -0
- ostium/web/dist/assets/DiagnosticsPage-GBMdduQn.js +1 -0
- ostium/web/dist/assets/DiagnosticsPage-chvywnsd.css +1 -0
- ostium/web/dist/assets/Dropdown-CwXzstQE.js +125 -0
- ostium/web/dist/assets/HomeGate-3C_4sckV.css +1 -0
- ostium/web/dist/assets/HomeGate-D4c3bUDh.js +1 -0
- ostium/web/dist/assets/InboundsPage-DhR9BG0G.js +1 -0
- ostium/web/dist/assets/InboundsPage-Du4FhhK7.css +1 -0
- ostium/web/dist/assets/InputNumber-je36FpXB.js +13 -0
- ostium/web/dist/assets/JobsPage-BfbdymTp.css +1 -0
- ostium/web/dist/assets/JobsPage-CELbAvc8.js +1 -0
- ostium/web/dist/assets/LoginPage-B0c_xiRn.css +1 -0
- ostium/web/dist/assets/LoginPage-D1M5lp_t.js +110 -0
- ostium/web/dist/assets/LogsPage-BDLfX0KX.js +1 -0
- ostium/web/dist/assets/LogsPage-BLPPfggF.css +1 -0
- ostium/web/dist/assets/NetworkPage-B83yUZtL.css +1 -0
- ostium/web/dist/assets/NetworkPage-DMjbNN8w.js +1 -0
- ostium/web/dist/assets/NotFoundPage-4Eqar04L.js +1 -0
- ostium/web/dist/assets/NotFoundPage-eEit74Eg.css +1 -0
- ostium/web/dist/assets/OBrandMark-B6ptsQ9Y.js +1 -0
- ostium/web/dist/assets/OBrandMark-CVWmop63.css +1 -0
- ostium/web/dist/assets/OCopyButton-B-oqlfqz.css +1 -0
- ostium/web/dist/assets/OCopyButton-B0BdYJ8p.js +1 -0
- ostium/web/dist/assets/OEmptyState-C8WDHXJd.css +1 -0
- ostium/web/dist/assets/OEmptyState-CuNuiClw.js +1 -0
- ostium/web/dist/assets/OErrorState-C3LiX1aU.js +1 -0
- ostium/web/dist/assets/OErrorState-C3iaLxYh.css +1 -0
- ostium/web/dist/assets/OLazyChart-BxXcJLKv.css +1 -0
- ostium/web/dist/assets/OLazyChart-CPui9WML.js +2 -0
- ostium/web/dist/assets/OPageHeader-BpyY4eEc.js +1 -0
- ostium/web/dist/assets/OPageHeader-BsAKYTkr.css +1 -0
- ostium/web/dist/assets/OQrModal-BBvUIRXZ.css +1 -0
- ostium/web/dist/assets/OQrModal-DRzK9PFl.js +1 -0
- ostium/web/dist/assets/OSectionCard-BEbjsw4P.css +1 -0
- ostium/web/dist/assets/OSectionCard-Dwixcexc.js +1 -0
- ostium/web/dist/assets/OStatCard-DMMkumPu.css +1 -0
- ostium/web/dist/assets/OStatCard-DihjcKRH.js +1 -0
- ostium/web/dist/assets/OTrafficChart-BmqMasLG.js +19 -0
- ostium/web/dist/assets/OTrafficChart-yKTAaNwo.css +1 -0
- ostium/web/dist/assets/OnboardingPage-CnC3Tkn8.js +1 -0
- ostium/web/dist/assets/OnboardingPage-Dao3P5YR.css +1 -0
- ostium/web/dist/assets/ProfilesPage-BW82LDzL.js +1 -0
- ostium/web/dist/assets/ProfilesPage-Bf9lzS6J.css +1 -0
- ostium/web/dist/assets/RadioGroup-DjGmk7eo.js +153 -0
- ostium/web/dist/assets/RoutingPage-CHDNOmbs.css +1 -0
- ostium/web/dist/assets/RoutingPage-TELCLabr.js +1 -0
- ostium/web/dist/assets/RulesPage-DM47ToQK.js +1 -0
- ostium/web/dist/assets/RulesPage-DeW2Bp-B.css +1 -0
- ostium/web/dist/assets/Select-IxrvO84D.js +410 -0
- ostium/web/dist/assets/SettingsPage-01lhFrbF.css +1 -0
- ostium/web/dist/assets/SettingsPage-CdaYFH0Z.js +1 -0
- ostium/web/dist/assets/SetupPage-B5T7fne1.css +1 -0
- ostium/web/dist/assets/SetupPage-BWRiRx0N.js +1 -0
- ostium/web/dist/assets/StatsPage-C_J3rnv5.css +1 -0
- ostium/web/dist/assets/StatsPage-CiP7SXHY.js +1 -0
- ostium/web/dist/assets/SubscriptionsPage-BSecexRk.css +1 -0
- ostium/web/dist/assets/SubscriptionsPage-Cdq6Y3T2.js +1 -0
- ostium/web/dist/assets/Switch-CX9oUrD6.js +102 -0
- ostium/web/dist/assets/SystemPage-Deu_D2H5.css +1 -0
- ostium/web/dist/assets/SystemPage-DqesWfD8.js +1 -0
- ostium/web/dist/assets/UsersPage-CDpf--BF.js +362 -0
- ostium/web/dist/assets/UsersPage-DtGDcfjr.css +1 -0
- ostium/web/dist/assets/_plugin-vue_export-helper-CtsCrH4a.js +3 -0
- ostium/web/dist/assets/arrow-left-D_hTaPYD.js +1 -0
- ostium/web/dist/assets/arrow-up-from-line-C0pEYqml.js +1 -0
- ostium/web/dist/assets/calendar-clock-DiHESNVh.js +1 -0
- ostium/web/dist/assets/check-DGkXO7lB.js +1 -0
- ostium/web/dist/assets/compass-L-dwGuZp.js +1 -0
- ostium/web/dist/assets/composables-BsF0VAcy.js +1 -0
- ostium/web/dist/assets/create-mR1YnL_8.js +117 -0
- ostium/web/dist/assets/createLucideIcon-CWlB5tHO.js +45 -0
- ostium/web/dist/assets/css-HHsd4Dgi.js +1 -0
- ostium/web/dist/assets/database-Cq48lz38.js +1 -0
- ostium/web/dist/assets/download-58fxtqjE.js +1 -0
- ostium/web/dist/assets/earth-BmbhRGrA.js +1 -0
- ostium/web/dist/assets/events-iRSPNrZu.js +1 -0
- ostium/web/dist/assets/get-DCmvXwyo.js +1 -0
- ostium/web/dist/assets/index-BF0kVdj-.css +1 -0
- ostium/web/dist/assets/index-ByVCKlLD.js +1318 -0
- ostium/web/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- ostium/web/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- ostium/web/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- ostium/web/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- ostium/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- ostium/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- ostium/web/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- ostium/web/dist/assets/key-round-pOGf6RQ8.js +1 -0
- ostium/web/dist/assets/lock-keyhole-CJztbacy.js +1 -0
- ostium/web/dist/assets/memory-stick-DFakatVS.js +1 -0
- ostium/web/dist/assets/misc-v7XGxIq4.js +1 -0
- ostium/web/dist/assets/pause-CmQQzZLQ.js +1 -0
- ostium/web/dist/assets/play-Dj9D01ba.js +1 -0
- ostium/web/dist/assets/plus-BS9m8U59.js +1 -0
- ostium/web/dist/assets/replaceable-Xwldo3FD.js +77 -0
- ostium/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- ostium/web/dist/assets/rotate-ccw-CxnBy7vT.js +1 -0
- ostium/web/dist/assets/route-DY54yEWA.js +1 -0
- ostium/web/dist/assets/server-DmdBrRlX.js +1 -0
- ostium/web/dist/assets/shield-question-mark-p_oiEXDj.js +1 -0
- ostium/web/dist/assets/sparkles-DwBhkDLT.js +1 -0
- ostium/web/dist/assets/terminal-DSrQLazn.js +1 -0
- ostium/web/dist/assets/trash-B4t97Yg6.js +1 -0
- ostium/web/dist/assets/use-message-DpuN2mQG.js +1 -0
- ostium/web/dist/assets/useBrowserNotifications-D7TM1fAV.js +1 -0
- ostium/web/dist/assets/useCopy-qMe7zkUi.js +1 -0
- ostium/web/dist/assets/user-round-BgbIfPpl.js +1 -0
- ostium/web/dist/assets/zap-mwHRUjCZ.js +1 -0
- ostium/web/dist/icons/apple-touch-icon.png +0 -0
- ostium/web/dist/icons/favicon.svg +14 -0
- ostium/web/dist/icons/icon-192.png +0 -0
- ostium/web/dist/icons/icon-512.png +0 -0
- ostium/web/dist/icons/maskable-512.png +0 -0
- ostium/web/dist/index.html +25 -0
- ostium/web/dist/manifest.webmanifest +21 -0
- ostium/web/dist/sw.js +54 -0
- ostium/web/dist/theme-init.js +6 -0
- ostium_ctl-0.1.0.dist-info/METADATA +903 -0
- ostium_ctl-0.1.0.dist-info/RECORD +203 -0
- ostium_ctl-0.1.0.dist-info/WHEEL +4 -0
- ostium_ctl-0.1.0.dist-info/entry_points.txt +3 -0
- ostium_ctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- ostium_ctl-0.1.0.dist-info/licenses/NOTICE +32 -0
ostium/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Ostium package metadata and public constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("ostium-ctl")
|
|
9
|
+
except PackageNotFoundError: # pragma: no cover - source checkout without install
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
APP_NAME = "Ostium"
|
|
13
|
+
API_PREFIX = "/api/v1"
|
|
14
|
+
CORE_API_VERSION = "v1"
|
|
15
|
+
PROFILE_FORMAT_VERSION = 1
|
|
16
|
+
USER_AGENT = f"Ostium/{__version__}"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"API_PREFIX",
|
|
20
|
+
"APP_NAME",
|
|
21
|
+
"CORE_API_VERSION",
|
|
22
|
+
"PROFILE_FORMAT_VERSION",
|
|
23
|
+
"USER_AGENT",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
ostium/__main__.py
ADDED
ostium/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
ostium/acme/__init__.py
ADDED
|
File without changes
|
ostium/acme/client.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Built-in ACME HTTP-01 client.
|
|
2
|
+
|
|
3
|
+
Only HTTP-01 is implemented by design (see PLAN.md §2.4). The challenge server
|
|
4
|
+
binds port 80 for the duration of issuance and serves exactly the requested key
|
|
5
|
+
authorizations, then shuts down.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import josepy as jose
|
|
19
|
+
from acme import challenges, client, errors, messages
|
|
20
|
+
from cryptography import x509
|
|
21
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
23
|
+
from cryptography.x509.oid import NameOID
|
|
24
|
+
|
|
25
|
+
from ostium.config import Settings
|
|
26
|
+
from ostium.services.crypto import SecretStore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AcmeError(Exception):
|
|
30
|
+
def __init__(self, message: str, code: str = "acme_error", status_code: int = 502) -> None:
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.code = code
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ChallengeStore:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.tokens: dict[str, str] = {}
|
|
39
|
+
self.lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
def put(self, token: str, key_authorization: str) -> None:
|
|
42
|
+
with self.lock:
|
|
43
|
+
self.tokens[token] = key_authorization
|
|
44
|
+
|
|
45
|
+
def get(self, token: str) -> str | None:
|
|
46
|
+
with self.lock:
|
|
47
|
+
return self.tokens.get(token)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _handler(store: _ChallengeStore) -> type[BaseHTTPRequestHandler]:
|
|
51
|
+
class Handler(BaseHTTPRequestHandler):
|
|
52
|
+
def do_GET(self) -> None:
|
|
53
|
+
prefix = "/.well-known/acme-challenge/"
|
|
54
|
+
if not self.path.startswith(prefix):
|
|
55
|
+
self.send_error(404)
|
|
56
|
+
return
|
|
57
|
+
token = self.path[len(prefix) :].split("?", 1)[0]
|
|
58
|
+
value = store.get(token)
|
|
59
|
+
if value is None:
|
|
60
|
+
self.send_error(404)
|
|
61
|
+
return
|
|
62
|
+
body = value.encode("ascii")
|
|
63
|
+
self.send_response(200)
|
|
64
|
+
self.send_header("Content-Type", "text/plain")
|
|
65
|
+
self.send_header("Content-Length", str(len(body)))
|
|
66
|
+
self.end_headers()
|
|
67
|
+
self.wfile.write(body)
|
|
68
|
+
|
|
69
|
+
def log_message(self, _format: str, *_args: Any) -> None:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
return Handler
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextlib.contextmanager
|
|
76
|
+
def challenge_server(port: int = 80) -> Iterator[_ChallengeStore]:
|
|
77
|
+
store = _ChallengeStore()
|
|
78
|
+
try:
|
|
79
|
+
server = ThreadingHTTPServer(("0.0.0.0", port), _handler(store))
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
raise AcmeError(
|
|
82
|
+
f"cannot bind port {port} for the ACME HTTP-01 challenge: {exc}", "port_80_unavailable"
|
|
83
|
+
) from exc
|
|
84
|
+
server.daemon_threads = True
|
|
85
|
+
thread = threading.Thread(target=server.serve_forever, name="acme-http01", daemon=True)
|
|
86
|
+
thread.start()
|
|
87
|
+
try:
|
|
88
|
+
yield store
|
|
89
|
+
finally:
|
|
90
|
+
server.shutdown()
|
|
91
|
+
server.server_close()
|
|
92
|
+
thread.join(timeout=5)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AcmeIssuer:
|
|
96
|
+
def __init__(self, settings: Settings, store: SecretStore) -> None:
|
|
97
|
+
self.settings = settings
|
|
98
|
+
self.store = store
|
|
99
|
+
|
|
100
|
+
def _directory_url(self) -> str:
|
|
101
|
+
if self.settings.acme_staging:
|
|
102
|
+
return "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
103
|
+
return self.settings.acme_directory
|
|
104
|
+
|
|
105
|
+
def _load_account_key(self) -> rsa.RSAPrivateKey:
|
|
106
|
+
# The account key lives in a small file next to the master key; it is
|
|
107
|
+
# encrypted at rest with the same master key.
|
|
108
|
+
path = self.settings.data_dir / "acme" / "account.key.enc"
|
|
109
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
if path.is_file():
|
|
111
|
+
pem = self.store.decrypt(
|
|
112
|
+
path.read_text(encoding="utf-8"),
|
|
113
|
+
table="settings",
|
|
114
|
+
record_id="acme",
|
|
115
|
+
field="account_key",
|
|
116
|
+
)
|
|
117
|
+
key = serialization.load_pem_private_key(pem.encode(), password=None)
|
|
118
|
+
assert isinstance(key, rsa.RSAPrivateKey)
|
|
119
|
+
return key
|
|
120
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
121
|
+
pem = key.private_bytes(
|
|
122
|
+
serialization.Encoding.PEM,
|
|
123
|
+
serialization.PrivateFormat.PKCS8,
|
|
124
|
+
serialization.NoEncryption(),
|
|
125
|
+
).decode()
|
|
126
|
+
path.write_text(
|
|
127
|
+
self.store.encrypt(pem, table="settings", record_id="acme", field="account_key"),
|
|
128
|
+
encoding="utf-8",
|
|
129
|
+
)
|
|
130
|
+
with contextlib.suppress(OSError):
|
|
131
|
+
path.chmod(0o600)
|
|
132
|
+
return key
|
|
133
|
+
|
|
134
|
+
async def issue(self, domain: str, *, timeout_seconds: int = 180) -> tuple[str, str]:
|
|
135
|
+
return await asyncio.to_thread(self._issue_sync, domain, timeout_seconds)
|
|
136
|
+
|
|
137
|
+
def _issue_sync(self, domain: str, timeout_seconds: int) -> tuple[str, str]:
|
|
138
|
+
from ostium.services.certificates import CertificateError, validate_domain
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
domain = validate_domain(domain)
|
|
142
|
+
except CertificateError as exc:
|
|
143
|
+
raise AcmeError(str(exc), "invalid_domain", 400) from exc
|
|
144
|
+
account_key = self._load_account_key()
|
|
145
|
+
acme_key = jose.JWKRSA(key=account_key) # type: ignore[attr-defined]
|
|
146
|
+
net = client.ClientNetwork(
|
|
147
|
+
acme_key,
|
|
148
|
+
user_agent="Ostium/0.1 (+https://github.com/ThzxxArt/Ostium)",
|
|
149
|
+
timeout=30,
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
directory = messages.Directory.from_json(net.get(self._directory_url()).json())
|
|
153
|
+
acme_client = client.ClientV2(directory, net)
|
|
154
|
+
with contextlib.suppress(errors.ConflictError):
|
|
155
|
+
acme_client.new_account(
|
|
156
|
+
messages.NewRegistration.from_data(
|
|
157
|
+
email=self.settings.acme_email, terms_of_service_agreed=True
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
cert_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
161
|
+
csr = (
|
|
162
|
+
x509.CertificateSigningRequestBuilder()
|
|
163
|
+
.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, domain)]))
|
|
164
|
+
.add_extension(x509.SubjectAlternativeName([x509.DNSName(domain)]), critical=False)
|
|
165
|
+
.sign(cert_key, hashes.SHA256())
|
|
166
|
+
)
|
|
167
|
+
order = acme_client.new_order(csr.public_bytes(serialization.Encoding.PEM))
|
|
168
|
+
with challenge_server(80) as store:
|
|
169
|
+
for authz in order.authorizations:
|
|
170
|
+
challenge = next(
|
|
171
|
+
(
|
|
172
|
+
item
|
|
173
|
+
for item in authz.body.challenges
|
|
174
|
+
if item.chall.typ == challenges.HTTP01.typ
|
|
175
|
+
),
|
|
176
|
+
None,
|
|
177
|
+
)
|
|
178
|
+
if challenge is None:
|
|
179
|
+
raise AcmeError(
|
|
180
|
+
"the ACME server did not offer HTTP-01 for "
|
|
181
|
+
f"{authz.body.identifier.value}"
|
|
182
|
+
)
|
|
183
|
+
response, validation = challenge.response_and_validation(acme_client.net.key)
|
|
184
|
+
store.put(challenge.chall.token, validation)
|
|
185
|
+
acme_client.answer_challenge(challenge, response)
|
|
186
|
+
deadline = dt.datetime.now(dt.UTC) + dt.timedelta(seconds=timeout_seconds)
|
|
187
|
+
finalized = acme_client.poll_and_finalize(order, deadline=deadline)
|
|
188
|
+
fullchain = finalized.fullchain_pem
|
|
189
|
+
if not fullchain:
|
|
190
|
+
raise AcmeError("ACME did not return a certificate chain")
|
|
191
|
+
key_pem = cert_key.private_bytes(
|
|
192
|
+
serialization.Encoding.PEM,
|
|
193
|
+
serialization.PrivateFormat.PKCS8,
|
|
194
|
+
serialization.NoEncryption(),
|
|
195
|
+
).decode()
|
|
196
|
+
return fullchain, key_pem
|
|
197
|
+
except AcmeError:
|
|
198
|
+
raise
|
|
199
|
+
except errors.Error as exc:
|
|
200
|
+
raise AcmeError(f"ACME request failed: {exc}") from exc
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
raise AcmeError(f"ACME issuance failed: {exc}") from exc
|
ostium/api/__init__.py
ADDED
|
File without changes
|