diff --git a/rest_api_debug_tool/app.py b/rest_api_debug_tool/app.py new file mode 100644 index 0000000..ad7d041 --- /dev/null +++ b/rest_api_debug_tool/app.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import json +import re +from typing import Any, Dict, Tuple + +import requests +from flask import Flask, jsonify, request, send_from_directory + +app = Flask(__name__, static_folder="static") + +HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +} + +PRIVATE_HOST_RE = re.compile(r"^(localhost|127\.0\.0\.1|0\.0\.0\.0)$", re.I) + + +def _filter_response_headers(headers: Dict[str, str]) -> Dict[str, str]: + out: Dict[str, str] = {} + for k, v in headers.items(): + if k.lower() in HOP_BY_HOP_HEADERS: + continue + out[k] = v + return out + + +def _safe_target_url(url: str) -> Tuple[bool, str]: + # Базовая защита от совсем странного ввода. + # Для локальной разработки можно оставлять шире, но хотя бы не даём file:// и т.п. + if not url: + return False, "Empty url" + if not (url.startswith("http://") or url.startswith("https://")): + return False, "Only http/https URLs are allowed" + return True, "" + + +@app.get("/") +def index(): + return send_from_directory(app.static_folder, "index.html") + + +@app.post("/proxy") +def proxy(): + data: Dict[str, Any] = request.get_json(silent=True) or {} + + url = str(data.get("url", "")).strip() + method = str(data.get("method", "GET")).upper().strip() + body_type = str(data.get("body_type", "none")).strip() + headers_in = data.get("headers") or {} + body_in = data.get("body", "") + + ok, err = _safe_target_url(url) + if not ok: + return jsonify({"error": err}), 400 + + if not isinstance(headers_in, dict): + return jsonify({"error": "headers must be an object/dict"}), 400 + + # Вырезаем хоп-бай-хоп заголовки + headers: Dict[str, str] = {} + for k, v in headers_in.items(): + if not isinstance(k, str): + continue + if k.lower() in HOP_BY_HOP_HEADERS: + continue + headers[k] = str(v) + + timeout_s = 30 + + try: + req_kwargs: Dict[str, Any] = { + "headers": headers, + "timeout": timeout_s, + "allow_redirects": False, + } + + if method in ("GET", "HEAD"): + # body не отправляем + pass + else: + if body_type == "json": + # body_in может быть строкой JSON или объектом + if isinstance(body_in, (dict, list)): + req_kwargs["json"] = body_in + else: + # строка + text = str(body_in) + if text.strip(): + req_kwargs["data"] = text.encode("utf-8") + else: + req_kwargs["data"] = b"" + # если Content-Type не задан — проставим + if not any(k.lower() == "content-type" for k in headers.keys()): + req_kwargs["headers"]["Content-Type"] = "application/json; charset=utf-8" + + elif body_type == "form": + # body_in ожидаем как строку "a=1\nb=2" или dict + if isinstance(body_in, dict): + req_kwargs["data"] = body_in + else: + lines = [l.strip() for l in str(body_in).splitlines() if l.strip()] + form: Dict[str, str] = {} + for line in lines: + if "=" not in line: + continue + k, v = line.split("=", 1) + form[k.strip()] = v.strip() + req_kwargs["data"] = form + if not any(k.lower() == "content-type" for k in headers.keys()): + req_kwargs["headers"]["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" + + elif body_type == "raw": + req_kwargs["data"] = (str(body_in)).encode("utf-8") + if not any(k.lower() == "content-type" for k in headers.keys()): + req_kwargs["headers"]["Content-Type"] = "text/plain; charset=utf-8" + else: + # none + pass + + resp = requests.request(method, url, **req_kwargs) + + resp_headers = _filter_response_headers(dict(resp.headers)) + content_type = resp.headers.get("content-type", "") + + # Возвращаем тело как текст (включая JSON строкой) + # Если бинарь — лучше base64, но ты писал что в основном JSON/текст + try: + resp_text = resp.text + except Exception: + resp_text = resp.content.decode("utf-8", errors="replace") + + return jsonify( + { + "status": resp.status_code, + "headers": resp_headers, + "content_type": content_type, + "body": resp_text, + } + ), 200 + + except requests.RequestException as e: + return jsonify({"error": f"Upstream request failed: {str(e)}"}), 502 + + +if __name__ == "__main__": + # http://127.0.0.1:8000 + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/rest_api_debug_tool/requirements.txt b/rest_api_debug_tool/requirements.txt new file mode 100644 index 0000000..ebbdb01 --- /dev/null +++ b/rest_api_debug_tool/requirements.txt @@ -0,0 +1,12 @@ +blinker==1.9.0 +certifi==2025.11.12 +charset-normalizer==3.4.4 +click==8.3.1 +Flask==3.1.2 +idna==3.11 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +requests==2.32.5 +urllib3==2.6.2 +Werkzeug==3.1.4 diff --git a/rest_api_debug_tool/static/index.html b/rest_api_debug_tool/static/index.html new file mode 100644 index 0000000..cac71af --- /dev/null +++ b/rest_api_debug_tool/static/index.html @@ -0,0 +1,454 @@ + + +
+ + +
+
+
+
+
+
+
+