from __future__ import annotations import argparse import hmac import json import os import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] CONNECTOR = ROOT / "plugins" / "1c" / "connector" if str(CONNECTOR) not in sys.path: sys.path.insert(0, str(CONNECTOR)) import repository_control # noqa: E402 def bearer_token() -> str: env_name = os.environ.get("ONEC_REPOSITORY_RUNNER_TOKEN_ENV", "ONEC_REPOSITORY_RUNNER_TOKEN") return os.environ.get(env_name, "") def authorized(header: str) -> bool: expected = bearer_token() if not expected: return os.environ.get("ONEC_REPOSITORY_RUNNER_ALLOW_UNAUTHENTICATED", "").strip().casefold() == "true" prefix = "Bearer " return header.startswith(prefix) and hmac.compare_digest(header[len(prefix):], expected) class Handler(BaseHTTPRequestHandler): server_version = "OneCRepositoryRunner/1" def _json(self, status: int, value: dict[str, Any]) -> None: body = json.dumps(value, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self) -> None: # noqa: N802 if self.path == "/healthz": self._json(200, {"status": "ok", "service": "onec-repository-runner", "auth_configured": bool(bearer_token())}) return self._json(404, {"status": "not_found"}) def do_POST(self) -> None: # noqa: N802 if self.path != "/repository/execute": self._json(404, {"status": "not_found"}) return if not authorized(self.headers.get("Authorization", "")): self._json(401, {"status": "unauthorized"}) return try: length = int(self.headers.get("Content-Length", "0")) if length <= 0 or length > 1024 * 1024: raise ValueError("invalid request size") payload = json.loads(self.rfile.read(length).decode("utf-8")) if not isinstance(payload, dict): raise ValueError("request must be an object") except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc: self._json(400, {"status": "invalid_request", "message": str(exc)}) return base_id = str(payload.get("base_id") or "").strip() action = str(payload.get("action") or "").strip().casefold() objects = payload.get("objects") or [] if not base_id or action not in {"report", "lock", "unlock", "commit"}: self._json(400, {"status": "invalid_request", "message": "base_id and a supported action are required"}) return if not isinstance(objects, list) or any(not isinstance(item, str) or not item.strip() for item in objects) or len(objects) > 10000: self._json(400, {"status": "invalid_request", "message": "objects must be a bounded string array"}) return config, error = repository_control.repository_config(base_id) if error: self._json(400, error) return if (config.get("runner") or {}).get("kind") != "local": self._json(400, {"status": "invalid_config", "message": "Windows runner base configuration must use runner.kind=local"}) return result = repository_control._execute_repository( base_id, config, action, 180, objects=objects, comment=str(payload.get("comment") or ""), keep_locked=payload.get("keep_locked") is True, ) self._json(200 if result.get("status") == "ok" else 409, result) def log_message(self, format: str, *args: Any) -> None: return def main() -> None: parser = argparse.ArgumentParser(description="Authenticated Windows runner for standard 1C Designer repository commands.") parser.add_argument("--host", default=os.environ.get("ONEC_REPOSITORY_RUNNER_HOST", "127.0.0.1")) parser.add_argument("--port", type=int, default=int(os.environ.get("ONEC_REPOSITORY_RUNNER_PORT", "8121"))) args = parser.parse_args() ThreadingHTTPServer((args.host, args.port), Handler).serve_forever() if __name__ == "__main__": main()