tigrbl_api_hpks 0.1.2.dev9__tar.gz → 0.1.2.dev11__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.
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/PKG-INFO +1 -1
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/pyproject.toml +1 -1
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/api.py +107 -84
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/ops/pks.py +10 -2
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/tables/openpgp_key.py +1 -1
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/LICENSE +0 -0
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/README.md +0 -0
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/__init__.py +0 -0
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/app.py +0 -0
- {tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/tables/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl_api_hpks"
|
|
3
|
-
version = "0.1.2.
|
|
3
|
+
version = "0.1.2.dev11"
|
|
4
4
|
description = "High-trust OpenPGP keyserver APIs built on the Tigrbl application engine."
|
|
5
5
|
license = { file = "LICENSE" }
|
|
6
6
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -5,17 +5,14 @@ from __future__ import annotations
|
|
|
5
5
|
import datetime as dt
|
|
6
6
|
import os
|
|
7
7
|
import tempfile
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
from urllib.parse import parse_qs
|
|
10
10
|
|
|
11
|
-
from tigrbl import TigrblApp
|
|
12
|
-
from tigrbl.router import
|
|
13
|
-
from tigrbl.
|
|
14
|
-
from tigrbl.engine.shortcuts import engine as build_engine
|
|
15
|
-
from tigrbl.responses import Response
|
|
11
|
+
from tigrbl import Router, TigrblApp, Response, Request
|
|
12
|
+
from tigrbl.decorators.router import route
|
|
13
|
+
from tigrbl.shortcuts.engine import engine as build_engine
|
|
16
14
|
from tigrbl.runtime.status import HTTPException
|
|
17
|
-
|
|
18
|
-
from tigrbl.requests import Request
|
|
15
|
+
|
|
19
16
|
|
|
20
17
|
from .ops import pks as pks_ops
|
|
21
18
|
from .tables import OpenPGPKey
|
|
@@ -30,6 +27,18 @@ def _normalize_fingerprint(value: str) -> str:
|
|
|
30
27
|
return stripped.upper()
|
|
31
28
|
|
|
32
29
|
|
|
30
|
+
def _query_first(request: Request, key: str, default: str = "") -> str:
|
|
31
|
+
"""Extract the first value for a query parameter."""
|
|
32
|
+
values = request.query.get(key, [])
|
|
33
|
+
return values[0] if values else default
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _path_segment(request: Request, index: int) -> str:
|
|
37
|
+
"""Extract a path segment by index (0-based, after splitting on '/')."""
|
|
38
|
+
parts = request.path.strip("/").split("/")
|
|
39
|
+
return parts[index] if index < len(parts) else ""
|
|
40
|
+
|
|
41
|
+
|
|
33
42
|
def _response_text(
|
|
34
43
|
content: str,
|
|
35
44
|
*,
|
|
@@ -87,17 +96,13 @@ def build_app(
|
|
|
87
96
|
|
|
88
97
|
router = Router(prefix="/pks")
|
|
89
98
|
|
|
90
|
-
async def get_session() -> AsyncIterator[Any]:
|
|
91
|
-
async with app.engine.asession() as session:
|
|
92
|
-
yield session
|
|
93
|
-
|
|
94
99
|
def _not_found(detail: str = "Not found") -> HTTPException:
|
|
95
100
|
return HTTPException(status_code=404, detail=detail, headers=HPKS_CORS_HEADERS)
|
|
96
101
|
|
|
102
|
+
# ---- Legacy HKP endpoints ----
|
|
103
|
+
|
|
97
104
|
@route(router, "/add", methods=["POST"])
|
|
98
|
-
async def legacy_add(
|
|
99
|
-
request: Request, *, db: Any = Depends(get_session)
|
|
100
|
-
) -> Response:
|
|
105
|
+
async def legacy_add(request: Request) -> Response:
|
|
101
106
|
body = request.body.decode("utf-8")
|
|
102
107
|
form_data = parse_qs(body, keep_blank_values=True)
|
|
103
108
|
keytext = form_data.get("keytext", [""])[0]
|
|
@@ -107,72 +112,76 @@ def build_app(
|
|
|
107
112
|
detail="keytext is required",
|
|
108
113
|
headers=HPKS_CORS_HEADERS,
|
|
109
114
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
async with app.engine.asession() as db:
|
|
116
|
+
try:
|
|
117
|
+
await pks_ops.ingest_armored(db=db, armored=keytext)
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
raise HTTPException(
|
|
120
|
+
status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
|
|
121
|
+
) from exc
|
|
116
122
|
return _response_text("OK\n", headers=HPKS_CORS_HEADERS)
|
|
117
123
|
|
|
118
124
|
@route(router, "/lookup", methods=["GET"])
|
|
119
|
-
async def legacy_lookup(request: Request
|
|
120
|
-
op = request
|
|
121
|
-
search = request
|
|
125
|
+
async def legacy_lookup(request: Request):
|
|
126
|
+
op = _query_first(request, "op")
|
|
127
|
+
search = _query_first(request, "search")
|
|
122
128
|
op_name = op.lower()
|
|
123
129
|
normalized = _normalize_fingerprint(search)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
record
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
record
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
async with app.engine.asession() as db:
|
|
131
|
+
if op_name == "index":
|
|
132
|
+
results = await pks_ops.search_index(db=db, search=search)
|
|
133
|
+
if not results:
|
|
134
|
+
raise _not_found()
|
|
135
|
+
body = pks_ops.render_legacy_index(results, search=search)
|
|
136
|
+
return _response_text(body, headers=HPKS_CORS_HEADERS)
|
|
137
|
+
if op_name == "get":
|
|
138
|
+
if len(normalized) <= 8:
|
|
139
|
+
raise _not_found()
|
|
140
|
+
record = await pks_ops.lookup_by_fingerprint(
|
|
141
|
+
db=db, fingerprint=normalized
|
|
142
|
+
)
|
|
143
|
+
if record is None:
|
|
144
|
+
raise _not_found()
|
|
145
|
+
return _response_text(
|
|
146
|
+
record.ascii_armored,
|
|
147
|
+
media_type="application/pgp-keys",
|
|
148
|
+
headers=HPKS_CORS_HEADERS,
|
|
149
|
+
)
|
|
150
|
+
if op_name == "hget":
|
|
151
|
+
record = await pks_ops.lookup_by_email_hash(db=db, email_hash=search)
|
|
152
|
+
if record is None:
|
|
153
|
+
raise _not_found()
|
|
154
|
+
return _response_text(
|
|
155
|
+
record.ascii_armored,
|
|
156
|
+
media_type="application/pgp-keys",
|
|
157
|
+
headers=HPKS_CORS_HEADERS,
|
|
158
|
+
)
|
|
150
159
|
raise HTTPException(
|
|
151
160
|
status_code=400,
|
|
152
161
|
detail=f"Unsupported legacy op '{op}'",
|
|
153
162
|
headers=HPKS_CORS_HEADERS,
|
|
154
163
|
)
|
|
155
164
|
|
|
165
|
+
# ---- V2 endpoints ----
|
|
166
|
+
|
|
167
|
+
# Pattern: /pks/{fingerprint} — the fingerprint is the 2nd segment
|
|
156
168
|
@route(router, "/{fingerprint}", methods=["POST"])
|
|
157
|
-
async def merge_fingerprint(
|
|
158
|
-
request
|
|
159
|
-
*,
|
|
160
|
-
fingerprint: str,
|
|
161
|
-
db: Any = Depends(get_session),
|
|
162
|
-
) -> Response:
|
|
169
|
+
async def merge_fingerprint(request: Request) -> Response:
|
|
170
|
+
fingerprint = _path_segment(request, 1)
|
|
163
171
|
payload = await request.json() or {}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
async with app.engine.asession() as db:
|
|
173
|
+
try:
|
|
174
|
+
record = await pks_ops.merge_json_payload(
|
|
175
|
+
db=db, fingerprint=fingerprint, payload=payload
|
|
176
|
+
)
|
|
177
|
+
except ValueError as exc:
|
|
178
|
+
raise HTTPException(
|
|
179
|
+
status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
|
|
180
|
+
) from exc
|
|
172
181
|
return _response_json(pks_ops.to_v2_document(record), headers=HPKS_CORS_HEADERS)
|
|
173
182
|
|
|
174
183
|
@route(router, "/v2/add", methods=["POST"])
|
|
175
|
-
async def v2_add(request: Request
|
|
184
|
+
async def v2_add(request: Request) -> Response:
|
|
176
185
|
content_type = request.headers.get("content-type", "")
|
|
177
186
|
if "application/pgp-keys" not in content_type:
|
|
178
187
|
raise HTTPException(
|
|
@@ -185,25 +194,32 @@ def build_app(
|
|
|
185
194
|
raise HTTPException(
|
|
186
195
|
status_code=400, detail="Empty payload", headers=HPKS_CORS_HEADERS
|
|
187
196
|
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
async with app.engine.asession() as db:
|
|
198
|
+
try:
|
|
199
|
+
summary = await pks_ops.ingest_binary(db=db, bundle=bundle)
|
|
200
|
+
except ValueError as exc:
|
|
201
|
+
raise HTTPException(
|
|
202
|
+
status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
|
|
203
|
+
) from exc
|
|
194
204
|
return _response_json(summary, status_code=202, headers=HPKS_CORS_HEADERS)
|
|
195
205
|
|
|
206
|
+
# Pattern: /pks/v2/index/{query} — the query is the 4th segment
|
|
196
207
|
@route(router, "/v2/index/{query}", methods=["GET"])
|
|
197
|
-
async def v2_index(
|
|
198
|
-
|
|
208
|
+
async def v2_index(request: Request) -> Response:
|
|
209
|
+
query = _path_segment(request, 3)
|
|
210
|
+
async with app.engine.asession() as db:
|
|
211
|
+
results = await pks_ops.search_index(db=db, search=query)
|
|
199
212
|
if not results:
|
|
200
213
|
raise _not_found()
|
|
201
214
|
payload = [pks_ops.to_v2_document(rec) for rec in results]
|
|
202
215
|
return _response_json(payload, headers=HPKS_CORS_HEADERS)
|
|
203
216
|
|
|
217
|
+
# Pattern: /pks/v2/vfpget/{fingerprint} — fingerprint is the 4th segment
|
|
204
218
|
@route(router, "/v2/vfpget/{fingerprint}", methods=["GET"])
|
|
205
|
-
async def vfpget(
|
|
206
|
-
|
|
219
|
+
async def vfpget(request: Request) -> Response:
|
|
220
|
+
fingerprint = _path_segment(request, 3)
|
|
221
|
+
async with app.engine.asession() as db:
|
|
222
|
+
record = await pks_ops.lookup_by_fingerprint(db=db, fingerprint=fingerprint)
|
|
207
223
|
if record is None:
|
|
208
224
|
raise _not_found()
|
|
209
225
|
return _response_binary(
|
|
@@ -212,9 +228,12 @@ def build_app(
|
|
|
212
228
|
headers=HPKS_CORS_HEADERS,
|
|
213
229
|
)
|
|
214
230
|
|
|
231
|
+
# Pattern: /pks/v2/kidget/{keyid} — keyid is the 4th segment
|
|
215
232
|
@route(router, "/v2/kidget/{keyid}", methods=["GET"])
|
|
216
|
-
async def kidget(
|
|
217
|
-
|
|
233
|
+
async def kidget(request: Request) -> Response:
|
|
234
|
+
keyid = _path_segment(request, 3)
|
|
235
|
+
async with app.engine.asession() as db:
|
|
236
|
+
record = await pks_ops.lookup_by_keyid(db=db, key_id=keyid)
|
|
218
237
|
if record is None or (record.version and record.version > 4):
|
|
219
238
|
raise _not_found()
|
|
220
239
|
return _response_binary(
|
|
@@ -223,9 +242,12 @@ def build_app(
|
|
|
223
242
|
headers=HPKS_CORS_HEADERS,
|
|
224
243
|
)
|
|
225
244
|
|
|
245
|
+
# Pattern: /pks/v2/authget/{identifier} — identifier is the 4th segment
|
|
226
246
|
@route(router, "/v2/authget/{identifier}", methods=["GET"])
|
|
227
|
-
async def authget(
|
|
228
|
-
|
|
247
|
+
async def authget(request: Request) -> Response:
|
|
248
|
+
identifier = _path_segment(request, 3)
|
|
249
|
+
async with app.engine.asession() as db:
|
|
250
|
+
matches = await pks_ops.search_index(db=db, search=identifier)
|
|
229
251
|
lowered = identifier.lower()
|
|
230
252
|
record = next(
|
|
231
253
|
(
|
|
@@ -243,10 +265,10 @@ def build_app(
|
|
|
243
265
|
headers=HPKS_CORS_HEADERS,
|
|
244
266
|
)
|
|
245
267
|
|
|
268
|
+
# Pattern: /pks/v2/prefixlog/{since} — since is the 4th segment
|
|
246
269
|
@route(router, "/v2/prefixlog/{since}", methods=["GET"])
|
|
247
|
-
async def prefixlog_route(
|
|
248
|
-
since
|
|
249
|
-
) -> Response:
|
|
270
|
+
async def prefixlog_route(request: Request) -> Response:
|
|
271
|
+
since = _path_segment(request, 3)
|
|
250
272
|
try:
|
|
251
273
|
parsed = dt.datetime.fromisoformat(since)
|
|
252
274
|
except ValueError:
|
|
@@ -260,12 +282,13 @@ def build_app(
|
|
|
260
282
|
) from None
|
|
261
283
|
if parsed.tzinfo is None:
|
|
262
284
|
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
263
|
-
|
|
285
|
+
async with app.engine.asession() as db:
|
|
286
|
+
prefixes = await pks_ops.prefix_log(db=db, since=parsed)
|
|
264
287
|
body = "\r\n".join(prefixes)
|
|
265
288
|
return _response_text(body, headers=HPKS_CORS_HEADERS)
|
|
266
289
|
|
|
267
290
|
@route(router, "/v2/tokensend", methods=["POST"])
|
|
268
|
-
async def tokensend(
|
|
291
|
+
async def tokensend(request: Request) -> Response:
|
|
269
292
|
return Response(status_code=501, headers=list(HPKS_CORS_HEADERS.items()))
|
|
270
293
|
|
|
271
294
|
app.include_router(router)
|
|
@@ -437,7 +437,11 @@ async def _merge_parsed_key(*, db: Any, parsed: ParsedKey) -> OpenPGPKey:
|
|
|
437
437
|
payload["bits"] = parsed.bits or existing.bits
|
|
438
438
|
payload["primary_uid"] = parsed.primary_uid or existing.primary_uid
|
|
439
439
|
payload["version"] = parsed.version or existing.version
|
|
440
|
-
ctx = {
|
|
440
|
+
ctx = {
|
|
441
|
+
"db": db,
|
|
442
|
+
"payload": payload,
|
|
443
|
+
"path_params": {"item_id": parsed.fingerprint},
|
|
444
|
+
}
|
|
441
445
|
record = await OpenPGPKey.handlers.merge.handler(ctx)
|
|
442
446
|
await _commit_session(db)
|
|
443
447
|
return record
|
|
@@ -540,7 +544,11 @@ async def merge_json_payload(
|
|
|
540
544
|
"primary_uid": primary_uid or existing.primary_uid,
|
|
541
545
|
"version": payload.get("version") or existing.version,
|
|
542
546
|
}
|
|
543
|
-
ctx = {
|
|
547
|
+
ctx = {
|
|
548
|
+
"db": db,
|
|
549
|
+
"payload": merged_payload,
|
|
550
|
+
"path_params": {"item_id": normalized},
|
|
551
|
+
}
|
|
544
552
|
record = await OpenPGPKey.handlers.merge.handler(ctx)
|
|
545
553
|
await _commit_session(db)
|
|
546
554
|
return record
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrbl_api_hpks-0.1.2.dev9 → tigrbl_api_hpks-0.1.2.dev11}/src/tigrbl_api_hpks/tables/__init__.py
RENAMED
|
File without changes
|