Этап 6: закалка — лимиты ADR-021, аудит модели угроз, сверка документов
Лимиты: все четыре правила ADR-021 — регистрация 5/час на IP, вход 10/10 мин на IP и ник, сообщения 30/мин, прочие изменяющие 60/мин; 429 с Retry-After; X-Real-IP читается только с loopback, иначе адрес соединения — иначе заголовок отменял бы лимит на IP; карты вёдер ограничены поколениями. Аудит нашёл то, что пропустили пять раундов ревью: ADR-056: nginx вёл access_log с IP и полными путями вопреки обещанию deploy.md. Ники и социальный граф ложились в /var/log/nginx рядом с чистым журналом bare. ADR-058: «выйти на других устройствах» не обрывал уже открытый SSE — отозванная сессия продолжала получать сообщения. ADR-059: промежуточный ключ комнаты был невосстановим. Участник, пропустивший офлайн два rekey подряд, навсегда не расшифровал бы сообщения среднего ключа — вопреки обещанию storage.md о повторной попытке после получения keyId. ADR-063: ACK уходил по одному на конверт, а не пачкой. Получатель в оживлённой комнате выедал общее ведро подтверждениями и упирался в 429 на всех изменяющих запросах, включая выход из комнаты: 116 отказов за прогон стало нулём. ADR-055, 057, 060, 061, 062: ключи вёдер и границы, +dirty у bare version, 403 unknown_device не хоронит сообщение, усечение имени в подсказке ввода, kdf как оракул после повышения цели KDF. Модель угроз пополнена тем, что действительно видит оператор: push-подписки лежат в базе открытым текстом, и вместе с VAPID-ключом с той же машины это произвольное уведомление на экране блокировки. README приведён к v1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -20,9 +21,11 @@ import (
|
||||
const origin = "https://bare.test"
|
||||
|
||||
// env — сервер на временной базе плюс журнал, в который он пишет.
|
||||
// Обработчик хранится своим типом: тестам нужен не только ServeHTTP,
|
||||
// но и остановка — Close и CloseStreams.
|
||||
type env struct {
|
||||
t *testing.T
|
||||
h http.Handler
|
||||
h *api.Handler
|
||||
st *store.Store
|
||||
log *syncLog
|
||||
srv *httptest.Server
|
||||
@@ -143,6 +146,33 @@ func withDevice(id string) func(*http.Request) {
|
||||
return func(r *http.Request) { r.Header.Set("X-Device", id) }
|
||||
}
|
||||
|
||||
// withRemote — адрес, с которого пришло соединение. От него зависят лимиты
|
||||
// на IP (ADR-021); httptest ставит всем один и тот же.
|
||||
func withRemote(addr string) func(*http.Request) {
|
||||
return func(r *http.Request) { r.RemoteAddr = addr }
|
||||
}
|
||||
|
||||
// withRealIP — заголовок, который ставит nginx. Читается, только если
|
||||
// соединение пришло с loopback (ADR-055).
|
||||
func withRealIP(ip string) func(*http.Request) {
|
||||
return func(r *http.Request) { r.Header.Set("X-Real-IP", ip) }
|
||||
}
|
||||
|
||||
// retryAfterOf — Retry-After ответа: целые секунды, не меньше одной
|
||||
// (docs/protocol.md, «Общие правила»).
|
||||
func retryAfterOf(t *testing.T, rec *httptest.ResponseRecorder) int {
|
||||
t.Helper()
|
||||
raw := rec.Header().Get("Retry-After")
|
||||
seconds, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("Retry-After: получено %q, ожидались целые секунды", raw)
|
||||
}
|
||||
if seconds < 1 {
|
||||
t.Errorf("Retry-After: получено %d, ожидалось не меньше 1", seconds)
|
||||
}
|
||||
return seconds
|
||||
}
|
||||
|
||||
func withOrigin(value string) func(*http.Request) {
|
||||
return func(r *http.Request) {
|
||||
if value == "" {
|
||||
|
||||
Reference in New Issue
Block a user