Spaces:
Running
Running
Download helpers/api.py from Leon4gr45/openoperator: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/Leon4gr45/openoperator/resolve/main/helpers/api.py
- Command line
-
hf download hf://spaces/Leon4gr45/openoperator/helpers/api.py
-
curl -L -o api.py https://huggingface.co/spaces/Leon4gr45/openoperator/resolve/main/helpers/api.py
11.6 kB
| from abc import abstractmethod | |
| import json | |
| import os | |
| import threading | |
| from urllib.parse import urlsplit, unquote | |
| from functools import wraps | |
| from pathlib import Path | |
| from typing import Union, Dict, Any | |
| from flask import ( | |
| Request, | |
| Response, | |
| jsonify, | |
| Flask, | |
| session, | |
| request, | |
| send_file, | |
| redirect, | |
| url_for, | |
| ) | |
| from werkzeug.wrappers.response import Response as BaseResponse | |
| from helpers.print_style import PrintStyle | |
| from helpers.errors import format_error | |
| from helpers import files, cache | |
| ThreadLockType = Union[threading.Lock, threading.RLock] | |
| CACHE_AREA = "api_handlers(api)" | |
| # cache.toggle_area(CACHE_AREA, False) # cache off for now | |
| Input = dict | |
| Output = Union[Dict[str, Any], Response] | |
| class ApiHandler: | |
| def __init__(self, app: Flask, thread_lock: ThreadLockType): | |
| self.app = app | |
| self.thread_lock = thread_lock | |
| def requires_loopback(cls) -> bool: | |
| return False | |
| def requires_api_key(cls) -> bool: | |
| return False | |
| def requires_auth(cls) -> bool: | |
| return True | |
| def get_methods(cls) -> list[str]: | |
| return ["POST"] | |
| def requires_csrf(cls) -> bool: | |
| return cls.requires_auth() | |
| async def process(self, input: Input, request: Request) -> Output: | |
| pass | |
| async def handle_request(self, request: Request) -> Response: | |
| try: | |
| # input data from request based on type | |
| input_data: Input = {} | |
| if request.is_json: | |
| try: | |
| if request.data: # Check if there's any data | |
| input_data = request.get_json() | |
| # If empty or not valid JSON, use empty dict | |
| except Exception as e: | |
| # Just log the error and continue with empty input | |
| PrintStyle().print(f"Error parsing JSON: {str(e)}") | |
| input_data = {} | |
| else: | |
| # input_data = {"data": request.get_data(as_text=True)} | |
| input_data = {} | |
| # process via handler | |
| output = await self.process(input_data, request) | |
| # return output based on type | |
| if isinstance(output, Response): | |
| return output | |
| else: | |
| response_json = json.dumps(output) | |
| return Response( | |
| response=response_json, status=200, mimetype="application/json" | |
| ) | |
| # return exceptions with 500 | |
| except Exception as e: | |
| error = format_error(e) | |
| PrintStyle.error(f"API error: {error}") | |
| return Response(response=error, status=500, mimetype="text/plain") | |
| # get context to run agent zero in | |
| def use_context(self, ctxid: str, create_if_not_exists: bool = True): | |
| from helpers.context_utils import use_context as _use_context | |
| return _use_context(self.thread_lock, ctxid, create_if_not_exists) | |
| from helpers.network import is_loopback_address | |
| def is_safe_next_url(value: str | None) -> bool: | |
| """Return True when value is a safe same-origin redirect target.""" | |
| if not value: | |
| return False | |
| if "\r" in value or "\n" in value: | |
| return False | |
| # Reject raw backslashes (browsers normalize `/\host` to `//host` -> external). | |
| if "\\" in value: | |
| return False | |
| # Decode percent-escapes so encoded backslashes (e.g. `%5C`) are caught too. | |
| decoded = unquote(value) | |
| if "\\" in decoded: | |
| return False | |
| parsed = urlsplit(decoded) | |
| if parsed.scheme or parsed.netloc: | |
| return False | |
| # Require an absolute path within this origin, but reject protocol-relative URLs. | |
| return parsed.path.startswith("/") and not parsed.path.startswith("//") | |
| def get_safe_next_url(value: str | None, fallback: str | None = None) -> str | None: | |
| """Return value if it is a safe next URL, otherwise return a safe fallback.""" | |
| if is_safe_next_url(value): | |
| return value | |
| if is_safe_next_url(fallback): | |
| return fallback | |
| return None | |
| def get_current_request_next_url() -> str: | |
| """Return the current request path/query as a safe relative redirect target.""" | |
| next_url = request.full_path if request.query_string else request.path | |
| return get_safe_next_url(next_url, url_for("serve_index")) or url_for("serve_index") | |
| def requires_api_key(f): | |
| async def decorated(*args, **kwargs): | |
| from helpers.settings import get_settings, resolve_api_token | |
| valid_api_key = resolve_api_token(get_settings().get("mcp_server_token")) | |
| req_json = request.get_json(silent=True) if request.is_json else None | |
| if api_key := request.headers.get("X-API-KEY"): | |
| if api_key != valid_api_key: | |
| return Response("Invalid API key", 401) | |
| elif req_json and isinstance(req_json, dict) and req_json.get("api_key"): | |
| api_key = req_json.get("api_key") | |
| if api_key != valid_api_key: | |
| return Response("Invalid API key", 401) | |
| else: | |
| return Response("API key required", 401) | |
| return await f(*args, **kwargs) | |
| return decorated | |
| def requires_loopback(f): | |
| async def decorated(*args, **kwargs): | |
| if not is_loopback_address(str(request.remote_addr)): | |
| return Response("Access denied.", 403, {}) | |
| return await f(*args, **kwargs) | |
| return decorated | |
| def requires_auth(f): | |
| async def decorated(*args, **kwargs): | |
| from helpers import login | |
| user_pass_hash = login.get_credentials_hash() | |
| if not user_pass_hash: | |
| return await f(*args, **kwargs) | |
| if session.get("authentication") != user_pass_hash: | |
| return redirect(url_for("login_handler", next=get_current_request_next_url())) | |
| return await f(*args, **kwargs) | |
| return decorated | |
| def csrf_protect(f): | |
| async def decorated(*args, **kwargs): | |
| from helpers import runtime | |
| token = session.get("csrf_token") | |
| header = request.headers.get("X-CSRF-Token") | |
| cookie = request.cookies.get("csrf_token_" + runtime.get_runtime_id()) | |
| sent = header or cookie | |
| if not token or not sent or token != sent: | |
| return Response("CSRF token missing or invalid", 403) | |
| return await f(*args, **kwargs) | |
| return decorated | |
| def register_api_route(app: Flask, lock: ThreadLockType) -> None: | |
| from helpers.modules import load_classes_from_file | |
| def _root_health(): | |
| sha = os.environ.get("OPENOPERATOR_SOURCE_SHA", "").strip() | |
| if not sha: | |
| for build_file in ("/a0/openoperator-build.json", files.get_abs_path("openoperator-build.json")): | |
| if files.exists(build_file): | |
| try: | |
| data = json.loads(files.read_file(build_file)) | |
| sha = str(data.get("source_sha") or "").strip() | |
| if sha: | |
| break | |
| except Exception: | |
| pass | |
| if not sha: | |
| try: | |
| from helpers.git import get_git_info | |
| sha = str(get_git_info().get("commit_hash") or "").strip() | |
| except Exception: | |
| sha = "" | |
| payload = {"status": "ok"} | |
| if sha: | |
| payload["sha"] = sha | |
| return Response(json.dumps(payload), status=200, mimetype="application/json") | |
| def _root_api_docs(): | |
| from helpers.modules import import_module | |
| api_docs_mod = import_module(files.get_abs_path("api/api_docs.py")) | |
| return Response(json.dumps(api_docs_mod.API_DOCS_PAYLOAD, indent=2), status=200, mimetype="application/json") | |
| from helpers import plugins | |
| async def _dispatch(path: str) -> BaseResponse: | |
| # Return cached wrapped handler if available | |
| cached = cache.get(CACHE_AREA, path) | |
| if cached is not None: | |
| return await cached() | |
| # Resolve file path for the handler | |
| # Try built-in and plugin api folders before the user fallback | |
| handler_cls: type[ApiHandler] | None = None | |
| # Check built-in python/api/<path>.py | |
| builtin_file = files.get_abs_path(f"api/{path}.py") | |
| if files.is_in_dir(builtin_file, files.get_abs_path("api")) and files.exists( | |
| builtin_file | |
| ): | |
| classes = load_classes_from_file(builtin_file, ApiHandler) | |
| if classes: | |
| handler_cls = classes[0] | |
| # Check plugin api folders: path format plugins/<plugin_name>/<handler> | |
| if handler_cls is None and path.startswith("plugins/"): | |
| parts = path.split("/", 2) | |
| if len(parts) == 3: | |
| _, plugin_name, handler_name = parts | |
| plugin_dir = plugins.find_plugin_dir(plugin_name) | |
| if plugin_dir: | |
| plugin_file = Path(plugin_dir) / "api" / f"{handler_name}.py" | |
| if plugin_file.is_file(): | |
| classes = load_classes_from_file(str(plugin_file), ApiHandler) | |
| if classes: | |
| handler_cls = classes[0] | |
| # Check user api/<path>.py | |
| if handler_cls is None: | |
| user_api_dir = files.get_abs_path(files.USER_DIR, files.API_DIR) | |
| user_file = files.get_abs_path(user_api_dir, f"{path}.py") | |
| if files.is_in_dir(user_file, user_api_dir) and files.exists(user_file): | |
| classes = load_classes_from_file(user_file, ApiHandler) | |
| if classes: | |
| handler_cls = classes[0] | |
| if handler_cls is None: | |
| return Response(f"API endpoint not found: {path}", 404) | |
| # Check method is allowed | |
| if request.method not in handler_cls.get_methods(): | |
| return Response(f"Method {request.method} not allowed for: {path}", 405) | |
| # Build handler call, wrapping with security decorators as required | |
| async def call_handler() -> BaseResponse: | |
| instance = handler_cls(app, lock) | |
| return await instance.handle_request(request=request) | |
| handler_fn = call_handler | |
| if handler_cls.requires_csrf(): | |
| handler_fn = csrf_protect(handler_fn) | |
| if handler_cls.requires_api_key(): | |
| handler_fn = requires_api_key(handler_fn) | |
| if handler_cls.requires_auth(): | |
| handler_fn = requires_auth(handler_fn) | |
| if handler_cls.requires_loopback(): | |
| handler_fn = requires_loopback(handler_fn) | |
| cache.add(CACHE_AREA, path, handler_fn) | |
| return await handler_fn() | |
| app.add_url_rule( | |
| "/api/<path:path>", | |
| "api_dispatch", | |
| _dispatch, | |
| methods=["GET", "POST", "PUT", "PATCH", "DELETE"], | |
| ) | |
| def register_watchdogs(): | |
| from helpers import watchdog | |
| from helpers.ws import CACHE_AREA as WS_CACHE_AREA | |
| def on_api_change(items: list[watchdog.WatchItem]): | |
| PrintStyle.debug("API endpoint watchdog triggered:", items) | |
| cache.clear(CACHE_AREA) | |
| cache.clear(WS_CACHE_AREA) | |
| watchdog.add_watchdog( | |
| "api_handlers", | |
| roots=[ | |
| files.get_abs_path(files.API_DIR), | |
| files.get_abs_path(files.USER_DIR, files.API_DIR), | |
| ], | |
| patterns=["*.py"], | |
| handler=on_api_change, | |
| ) | |