@@ -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 %d MB, 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 %d MB, 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