diff --git a/.gitignore b/.gitignore index 1484f24..b84d18a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /.venv __pycache__/ *.egg-info/ -provisioner.password diff --git a/Containerfile b/Containerfile index a0a6872..a3a08aa 100644 --- a/Containerfile +++ b/Containerfile @@ -17,12 +17,9 @@ RUN --mount=type=cache,target=/var/cache/apt \ RUN --mount=from=build,source=/tmp/build/dist,target=/tmp/wheels \ python3 -m pip install -f /tmp/wheels \ dch_webhooks \ - python-multipart \ uvicorn \ && : -COPY --from=docker.io/smallstep/step-cli:0.25.0 /usr/local/bin/step /usr/local/bin/step - USER 1000:1000 CMD ["tini", "/usr/local/bin/uvicorn", "dch_webhooks:app"] diff --git a/dch_webhooks.py b/dch_webhooks.py index 50f85ca..d5e9b42 100644 --- a/dch_webhooks.py +++ b/dch_webhooks.py @@ -1,14 +1,10 @@ -import asyncio -import base64 import datetime import importlib.metadata import logging import os import re -import tempfile -from pathlib import Path from types import TracebackType -from typing import Annotated, Optional, Self, Type +from typing import Optional, Self, Type import fastapi import httpx @@ -38,17 +34,10 @@ EXCLUDE_DESCRIPTION_WORDS = { 'the', } -ALLOW_RESIGNING_CERTS = os.environ.get('ALLOW_RESIGNING_CERTS') == '1' FIREFLY_URL = os.environ.get( 'FIREFLY_URL', 'http://firefly-iii', ) -MAX_KEY_FILE_SIZE = int( - os.environ.get( - 'MAX_KEY_FILE_SIZE', - 8192, - ) -) MAX_DOCUMENT_SIZE = int( os.environ.get( 'MAX_DOCUMENT_SIZE', @@ -61,10 +50,6 @@ PAPERLESS_URL = os.environ.get( ) -class SignError(Exception): - ... - - class FireflyIIITransactionSplit(pydantic.BaseModel): type: str date: datetime.datetime @@ -93,12 +78,6 @@ class PaperlessNgxSearchResults(pydantic.BaseModel): results: list[PaperlessNgxDocument] -class SSHKeySignResponse(pydantic.BaseModel): - success: bool - errors: Optional[list[str]] = None - certificates: Optional[dict[str, str]] - - class HttpxClientMixin: def __init__(self) -> None: super().__init__() @@ -165,13 +144,9 @@ class Firefly(HttpxClientMixin): rbody = r.json() attachment = rbody['data'] url = f'{FIREFLY_URL}/api/v1/attachments/{attachment["id"]}/upload' - r = await self.client.post( - url, - content=doc, - headers={ - 'Content-Type': 'application/octet-stream', - }, - ) + r = await self.client.post(url, content=doc, headers={ + 'Content-Type': 'application/octet-stream', + }) r.raise_for_status() @@ -272,7 +247,9 @@ class Paperless(HttpxClientMixin): MAX_DOCUMENT_SIZE, ) continue - docs.append((response_filename(r), doc.title, await r.aread())) + docs.append( + (response_filename(r), doc.title, await r.aread()) + ) return docs @@ -302,12 +279,6 @@ async def handle_firefly_transaction(xact: FireflyIIITransaction) -> None: ) -async def check_host(hostname: str) -> bool: - cmd = ['step', 'ssh', 'check-host', hostname] - p = await asyncio.create_subprocess_exec(*cmd) - return await p.wait() == 0 - - def clean_description(text: str) -> str: matches = DESCRIPTION_CLEAN_PATTERN.sub('', text.lower()) if not matches: @@ -337,56 +308,6 @@ def response_filename(response: httpx.Response) -> str: return response.url.path.rstrip('/').rsplit('/', 1)[-1] -async def sign_key(hostname, path: Path) -> tuple[str, str]: - cmd = ['step', 'ssh', 'certificate', '--sign', '--host', hostname, path] - p = await asyncio.create_subprocess_exec( - *cmd, - stdin=asyncio.subprocess.DEVNULL, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - ) - p_log = log.getChild('step') - assert p.stdout - buf = bytearray() - while line := await p.stdout.readline(): - buf += line - p_log.info(line.rstrip().decode('utf-8', 'replace')) - rc = await p.wait() - if rc != 0: - raise SignError( - f'Signing failed: process returned exit code {rc}: ' - f'{buf.decode("utf-8")}' - ) - cert_path = path.parent / f'{path.stem}-cert.pub' - log.info( - 'Successfully signed %s for %s as %s', - path.name, - hostname, - cert_path.name, - ) - with cert_path.open('r') as f: - cert = await asyncio.to_thread(f.read) - return (cert_path.name, cert) - - -async def sign_uploaded_key( - hostname: str, f: fastapi.UploadFile -) -> tuple[str, str]: - if f.size > MAX_KEY_FILE_SIZE: - raise SignError( - f'Refusing to sign key {f.filename}: file too large ' - f'({f.size} bytes, max {MAX_KEY_FILE_SIZE}' - ) - with tempfile.TemporaryDirectory() as t: - path = Path(t) / f.filename - with path.open('wb') as o: - d = await f.read(MAX_KEY_FILE_SIZE) - if f.headers.get('Content-Transfer-Encoding') == 'base64': - d = base64.b64decode(d) - await asyncio.to_thread(o.write, d) - return await sign_key(hostname, path) - - app = fastapi.FastAPI( name=DIST['Name'], version=DIST['Version'], @@ -410,41 +331,3 @@ def status() -> str: @app.post('/hooks/firefly-iii/create') async def firefly_iii_create(hook: FireflyIIIWebhook) -> None: await handle_firefly_transaction(hook.content) - - -@app.post('/sshkeys/sign', response_model=SSHKeySignResponse) -async def sign_ssh_keys( - response: fastapi.Response, - hostname: Annotated[str, fastapi.Form()], - keys: list[fastapi.UploadFile], -): - errors = [] - certificates = {} - if '.' not in hostname: - errors.append( - f'Cannot sign certificate for Single-label hostname {hostname}' - ) - if await check_host(hostname): - msg = f'{hostname} already has a signed certificate' - if ALLOW_RESIGNING_CERTS: - log.warning('%s', msg) - else: - log.error('%s', msg) - errors.append(msg) - if not errors: - tasks = [sign_uploaded_key(hostname, k) for k in keys] - for coro in asyncio.as_completed(tasks): - try: - name, cert = await coro - except Exception as e: - log.error('%s', e) - errors.append(str(e)) - else: - certificates[name] = cert - if errors: - response.status_code = fastapi.status.HTTP_400_BAD_REQUEST - return SSHKeySignResponse( - success=not errors, - errors=errors or None, - certificates=certificates or None, - )