feat: integrate upload-platform v0.2.2 into drhider (v0.0.78)
Deploy drhider / validate (push) Canceled after 0s

- Update upload/ module to v0.2.2 with modular Layer 1 (FilePicker) and Layer 2 (Streaming transit upload)
- Replace legacy manual file table in site/templates/index.html with FilePicker.initFilePicker
- Wire uploadViaVM with per-file status updates and abort signal support
- Add dist bundles to dist/ and site/static/dist/ with routes in site/routes/main_bp.py
- Add test_hardening.py and test_safe_name.py from upload-platform
- Bump version to 0.0.78 in site/app.py
- Document integration plan and report in History/upload-integration/
This commit is contained in:
Repinoid
2026-09-15 12:23:31 +03:00
parent 3fd6f1cffc
commit 49bb4d70d8
47 changed files with 6355 additions and 1561 deletions
+2 -15
View File
@@ -1,20 +1,7 @@
"""Слой 2 (бэк): переиспользуемый Blueprint закачки через ВМ.
Использование:
from upload.backend.upload_refs import create_upload_refs_blueprint
app.register_blueprint(create_upload_refs_blueprint(cfg))
"""
"""upload_refs — Flask Blueprint для приёма ссылок и pull с ВМ-буфера."""
from .blueprint import create_upload_refs_blueprint
from .safe_name import safe_name
from .pull_file import pull_file
from .config import PULL_RETRIES, PULL_RETRY_DELAY, VM_UPLOAD_PREFIX
__all__ = [
"create_upload_refs_blueprint",
"safe_name",
"pull_file",
"PULL_RETRIES",
"PULL_RETRY_DELAY",
"VM_UPLOAD_PREFIX",
]
__all__ = ["create_upload_refs_blueprint", "safe_name", "pull_file"]
+42 -20
View File
@@ -1,17 +1,20 @@
"""Переиспользуемый Blueprint слоя 2: POST /upload_refs (pull с ВМ-буфера в сессию).
Поведение 1:1 с drhider v0.0.75 (site/routes/api_bp.py):
- _safe_name (path traversal)
- SSRF-валидация url.startswith(VM_UPLOAD_PREFIX)
- лимит на один файл -> delete+skip
- pull с ретраями
Поведение:
- safe_name (защита от path traversal)
- SSRF-валидация url.startswith(vm_prefix)
- лимит на один файл -> delete + skip
- pull с ретраями через httpx stream
- лимит сессии -> skip; отсутствие сессии -> 404
- delete url с ВМ (best-effort)
- поддержка пофайлового транзита и пачек
- опциональный callback для интеграции/эмуляции Слоя 3 (on_file_received)
"""
import httpx
import logging
from flask import Blueprint, request, jsonify
from urllib.parse import urlsplit
from flask import Blueprint, request, jsonify, current_app
from ..session import (create_session, add_file, get_files, file_count,
MAX_FILE_BYTES, configure)
@@ -22,7 +25,7 @@ from .pull_file import pull_file
log = logging.getLogger("upload.upload_refs")
def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
def create_upload_refs_blueprint(cfg: dict = None) -> Blueprint:
"""Создать Blueprint с эндпоинтом upload_refs.
cfg (все ключи опциональны, есть дефолты):
@@ -34,13 +37,16 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
pullRetries (int) — ретраи pull
pullRetryDelay (int) — пауза между ретраями (сек)
pullTimeout (int) — таймаут одного GET pull
onFileReceived (func) — опциональный callback (sid, name, content) для Слоя 3
"""
cfg = cfg or {}
prefix = cfg.get("apiPrefix", "/api")
vm_prefix = cfg.get("vmUploadPrefix", VM_UPLOAD_PREFIX)
max_file_bytes = cfg.get("maxFileBytes", MAX_FILE_BYTES)
pull_retries = cfg.get("pullRetries", PULL_RETRIES)
pull_delay = cfg.get("pullRetryDelay", PULL_RETRY_DELAY)
pull_timeout = cfg.get("pullTimeout", 120)
on_file_received = cfg.get("onFileReceived")
# Применить лимиты сессии/TTL из конфига (глобально для всех сессий)
configure(
@@ -49,6 +55,8 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
ttl_seconds=cfg.get("ttlSeconds"),
)
is_path_only_prefix = vm_prefix.startswith("/")
bp = Blueprint("upload_refs", __name__, url_prefix=prefix)
@bp.route("/upload_refs", methods=["POST"])
@@ -56,9 +64,9 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
"""Принять ссылки на файлы (загружены на ВМ-буфер), забрать по egress.
Вход: JSON {"session": "...", "files": [{"name": str, "size": int, "url": str}]}.
Каждый файл тянется ИСХОДЯЩИМ GET'ом с ВМ (egress не ограничен шлюзом),
Каждый файл тянется исходящим GET с ВМ (egress не ограничен шлюзом),
читается по частям (stream), кладётся в сессию. После успешного pull файл
удаляется с ВМ (best-effort; TTL-чистка на ВМ тоже есть).
удаляется с ВМ (DELETE).
"""
data = request.get_json(silent=True) or {}
sid = data.get("session") or create_session()
@@ -67,18 +75,26 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
log.warning("upload_refs: no files, sid=%s", sid)
return jsonify({"ok": False, "error": "No files"}), 400
added = 0
transport = cfg.get("httpxTransport") or current_app.config.get("UPLOAD_HTTPX_TRANSPORT")
try:
with httpx.Client(timeout=pull_timeout, follow_redirects=True) as client:
with httpx.Client(transport=transport, timeout=pull_timeout, follow_redirects=True) as client:
for ref in refs:
name = safe_name(ref.get("name") or "")
url = ref.get("url")
url = ref.get("url") or ""
if not name or not url:
continue
# SSRF-защита: тянуть можно ТОЛЬКО с доверенного ВМ-буфера
if not url.startswith(vm_prefix):
log.warning("upload_refs: unsafe URL, skip sid=%s url=%r", sid, url)
continue
# Лимит на один файл: сверх лимита — пропускаем (не участвует)
if is_path_only_prefix:
if not urlsplit(url).path.startswith(vm_prefix):
log.warning("upload_refs: unsafe URL path, skip sid=%s url=%r", sid, url)
continue
if not url.startswith(("http://", "https://")):
url = request.host_url.rstrip("/") + ("/" if not url.startswith("/") else "") + url
else:
if not url.startswith(vm_prefix):
log.warning("upload_refs: unsafe URL, skip sid=%s url=%r", sid, url)
continue
# Лимит на один файл: сверх лимита — пропускаем
if (ref.get("size") or 0) > max_file_bytes:
log.warning("upload_refs: file exceeds %dMB, skip sid=%s file=%r size=%s",
max_file_bytes // (1024 * 1024), sid, name, ref.get("size"))
@@ -87,8 +103,8 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
except Exception:
pass
continue
# Pull с ретраями: разовые DNS/сетевые сбои не роняют всю загрузку
content = pull_file(client, url, pull_retries, pull_delay, sid=sid, name=name) # бросает при неудаче всех попыток
# Pull с ретраями
content = pull_file(client, url, pull_retries, pull_delay, sid=sid, name=name)
log.info("upload_refs: pulled sid=%s file=%r size=%d", sid, name, len(content))
if len(content) > max_file_bytes:
log.warning("upload_refs: pulled file exceeds %dMB, skip sid=%s file=%r size=%d",
@@ -99,7 +115,6 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
pass
continue
if not add_file(sid, name, content):
# Различить: сессия исчезла vs превышен суммарный лимит сессии
if get_files(sid) is None:
log.warning("upload_refs: session not found, sid=%s file=%r", sid, name)
return jsonify({"ok": False, "error": "Session not found"}), 404
@@ -109,15 +124,22 @@ def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
except Exception:
pass
continue
# Успешно добавлен в сессию — удаляем с ВМ-буфера
try:
client.delete(url) # убрать файл с ВМ после загрузки
client.delete(url)
except Exception:
pass
# Если передан callback для Слоя 3 (эмуляция или реальный процессинг)
if callable(on_file_received):
try:
on_file_received(sid, name, content)
except Exception as cb_err:
log.warning("upload_refs: on_file_received callback error: %r", cb_err)
added += 1
except Exception as e:
log.error("upload_refs: pull error sid=%s: %r", sid, e)
return jsonify({"ok": False, "error": "Pull failed: %s" % e}), 502
log.info("upload_refs: done sid=%s added=%d total=%d", sid, added, file_count(sid))
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
return jsonify({"ok": True, "session": sid, "count": file_count(sid), "added": added})
return bp
+1 -1
View File
@@ -7,5 +7,5 @@
PULL_RETRIES = 3
PULL_RETRY_DELAY = 2 # секунды между попытками
# Доверенный префикс ВМ-буфера — валидация URL при pull (защита от SSRF)
# Доверенный префикс ВМ-буфера по умолчанию — валидация URL при pull (защита от SSRF)
VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"