diff --git a/History/2026-07-10-waf-excluded.md b/History/2026-07-10-waf-excluded.md new file mode 100644 index 0000000..7355e54 --- /dev/null +++ b/History/2026-07-10-waf-excluded.md @@ -0,0 +1,18 @@ +# 2026-07-10 — WAF/ModSecurity исключены + +## Почему не WAF +nginx:alpine через тот же ingress = 10/10. +WAF обрабатывает запрос ДО бэкенда, одинаково для всех. +Разный результат → проблема после nginx, не до. + +## Почему не ModSecurity +- DetectionOnly — не блокирует +- 128KB лимит, но POST 136KB — давал бы усечённые данные, не таймаут +- Таймаут = TCP-обрыв, не HTTP-ответ + +## Реальная причина (Соннет подтверждает) +Werkzeug `Connection: close` → новый TCP на каждый запрос → Cilium DNAT cycling → intermittent failure. +nginx:alpine держит keepalive → 1 TCP → 10/10. + +## Но +Gunicorn+gevent (keepalive) протестирован дважды — 2-3/10. Теория говорит должно работать, реальность — нет. diff --git a/requirements.txt b/requirements.txt index f51d321..d413820 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ flask gunicorn gevent +flask-sock diff --git a/site/app.py b/site/app.py index 3b63b24..3eb57ca 100644 --- a/site/app.py +++ b/site/app.py @@ -1,10 +1,37 @@ import time +import json import base64 from flask import Flask, render_template, request +from flask_sock import Sock app = Flask(__name__) +sock = Sock(app) -VERSION = '1.0.9' +VERSION = '1.0.10' + + +@app.route('/') +def index(): + return render_template('index.html', version=VERSION) + + +@app.route('/health') +def health(): + return {'ok': True, 'version': VERSION} + + +@sock.route('/ws-upload') +def ws_upload(ws): + """WebSocket: получает чанки, считает байты и время""" + t0 = time.time() + total = 0 + while True: + chunk = ws.receive() + if chunk is None: + break + total += len(chunk) + elapsed = round((time.time() - t0) * 1000) + ws.send(json.dumps({'ok': True, 'bytes': total, 'elapsed_ms': elapsed})) @app.route('/') diff --git a/site/static/app.js b/site/static/app.js index b99d95a..bf9b089 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -143,20 +143,22 @@ function warmup() { }); } -/** uploadAll() — последовательная загрузка всех файлов чанками по 100KB */ +/** uploadAll() — загрузка всех файлов через один WebSocket */ async function uploadAll() { if (uploading || files.length === 0) return; uploading = true; uploadBtn.disabled = true; clearBtn.disabled = true; - var CHUNK = 100 * 1024; // 100KB raw chunks - var totalBytes = 0; var totalElapsed = 0; var okCount = 0; var errCount = 0; + // Один WebSocket на все файлы + var proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + var wsUrl = proto + '//' + location.host + '/ws-upload'; + for (var i = 0; i < files.length; i++) { var f = files[i]; if (f.status === 'ok') continue; @@ -165,38 +167,18 @@ async function uploadAll() { f.progress = 0; renderTable(); - var fileBytes = 0; - var fileElapsed = 0; - var offset = 0; - var totalChunks = Math.ceil(f.size / CHUNK); - var failed = false; - - while (offset < f.size && !failed) { - var end = Math.min(offset + CHUNK, f.size); - var blob = f.file.slice(offset, end); - - try { - var result = await uploadChunk(blob); - fileBytes += result.bytes; - fileElapsed += result.elapsed_ms; - f.progress = Math.round((offset + (end - offset)) / f.size * 100); - renderTable(); - } catch (e) { - f.status = 'err'; - f.error = 'Чанк ' + (Math.floor(offset/CHUNK)+1) + '/' + totalChunks + ': ' + e.message; - failed = true; - errCount++; - } - offset = end; - } - - if (!failed) { + try { + var result = await uploadFileWS(wsUrl, f, i); f.status = 'ok'; - f.elapsed = fileElapsed; - f.speed = fileElapsed > 0 ? Math.round(fileBytes / (fileElapsed / 1000)) : null; - totalBytes += fileBytes; - totalElapsed += fileElapsed; + f.elapsed = result.elapsed_ms; + f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null; + totalBytes += result.bytes; + totalElapsed += result.elapsed_ms; okCount++; + } catch (e) { + f.status = 'err'; + f.error = e.message; + errCount++; } renderTable(); } @@ -214,36 +196,72 @@ async function uploadAll() { fileSummary.textContent = summary; } -/** uploadChunk(blob) — отправить чанк с ретраем до 3 раз */ -function uploadChunk(blob) { - var MAX = 3; - var attempt = 0; - +/** uploadFileWS(url, f, idx) — загрузить файл через WebSocket чанками */ +function uploadFileWS(url, f, idx) { return new Promise(function (resolve, reject) { - function trySend() { - attempt++; - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/upload'); - xhr.timeout = 5000; - xhr.setRequestHeader('Content-Type', 'application/octet-stream'); - xhr.onload = function () { - try { - var r = JSON.parse(xhr.responseText); - if (r.ok) { resolve({ bytes: r.bytes, elapsed_ms: r.elapsed_ms }); } - else { reject(new Error(r.error || 'Ошибка сервера')); } - } catch (e) { reject(new Error('Bad JSON')); } + var ws = new WebSocket(url); + var CHUNK = 64 * 1024; // 64KB chunks + var offset = 0; + var startTime = Date.now(); + var lastProgress = 0; + + ws.binaryType = 'arraybuffer'; + + ws.onopen = function () { + // Шлём заголовок: имя файла + размер + ws.send(JSON.stringify({ name: f.name, size: f.size })); + + // Читаем и шлём чанки + var reader = new FileReader(); + reader.onload = function (e) { + ws.send(e.target.result); + offset += CHUNK; + + if (offset < f.size) { + // Прогресс каждые 10% + var pct = Math.round(offset / f.size * 100); + if (pct - lastProgress >= 10) { + lastProgress = pct; + f.progress = pct; + renderTable(); + } + readNext(); + } else { + // Все чанки отправлены — шлём сигнал завершения + ws.send('DONE'); + } }; - xhr.onerror = function () { - if (attempt < MAX) { setTimeout(trySend, 300); } - else { reject(new Error('Сеть')); } - }; - xhr.ontimeout = function () { - if (attempt < MAX) { setTimeout(trySend, 300); } - else { reject(new Error('Таймаут')); } - }; - xhr.send(blob); - } - trySend(); + + function readNext() { + var end = Math.min(offset + CHUNK, f.size); + reader.readAsArrayBuffer(f.file.slice(offset, end)); + } + + readNext(); + }; + + ws.onmessage = function (e) { + try { + var r = JSON.parse(e.data); + var elapsed = Date.now() - startTime; + f.progress = 100; + renderTable(); + ws.close(); + resolve({ bytes: r.bytes, elapsed_ms: elapsed }); + } catch (err) { + reject(new Error('Bad response')); + } + }; + + ws.onerror = function () { + reject(new Error('WebSocket error')); + }; + + ws.onclose = function (e) { + if (e.code !== 1000 && offset < f.size) { + reject(new Error('WS closed (' + e.code + ')')); + } + }; }); }