Files
transcriber/internal/controller/http/rate_limit.go
T
av 7f33c957e5 вход переехал на доверенный заголовок Authelia вместо собственного OIDC
- пришедшего называет заголовок Remote-User от прокси, и верят ему только с
  адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни
  корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе
- учётная запись заводится первым обращением: EnsureUser в пакете хранилища,
  шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users
- cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт
  унаследованный DL3066 — пользователь образа назван числом
2026-08-22 20:24:22 +03:00

93 lines
4.9 KiB
Go

package http
import (
"fmt"
"github.com/pocketbase/pocketbase/core"
)
// Своё правило ограничителя частоты под корнем приложения.
//
// Заводится потому, что правило хранилища настроено на **его** корень и наших
// адресов больше не покрывает: приложение уехало в своё пространство, и вместе с
// переездом ограничитель перестал бы существовать для него вовсе. Потеря тихая —
// заметить её нечем, пока кто-нибудь не начнёт опрашивать карточку в цикле.
//
// Числа скромные намеренно: сервисом пользуются единицы человек, а экран
// опрашивает карточку, пока запись идёт по конвейеру. Из них же выводится
// частота опроса, которую сервис объявляет приложению, — два числа об одном и
// том же разъехались бы при первой правке.
const (
appRateMaxRequests = 120
appRateWindowSec = 60
)
// ApplyAppRateLimit ставит правило ограничителя на корень приложения.
//
// Правило приводится к настройке при каждом подъёме, как и настройки провайдера:
// применённый шаг схемы не переписывается, а настройки хранилища живут в базе, и
// правило, положенное однажды, не пережило бы ни правки числа, ни чистого
// каталога данных.
func ApplyAppRateLimit(app core.App) error {
settings := app.Settings()
rule := core.RateLimitRule{
Label: AppRoot + "/",
MaxRequests: appRateMaxRequests,
Duration: appRateWindowSec,
}
replaced := false
for i, existing := range settings.RateLimits.Rules {
if existing.Label == rule.Label {
settings.RateLimits.Rules[i] = rule
replaced = true
break
}
}
if !replaced {
settings.RateLimits.Rules = append(settings.RateLimits.Rules, rule)
}
settings.RateLimits.Enabled = true
if err := app.Save(settings); err != nil {
return fmt.Errorf("failed to apply app rate limit: %w", err)
}
return nil
}
// ApplyTrustedProxyHeaders называет хранилищу заголовок, из которого брать
// адрес спрашивающего.
//
// Без этого ограничитель частоты ключует счётчик адресом **пира**, а пир с
// переездом входа на заголовок всегда один и тот же — обратный прокси. Бюджет
// в этом случае общий на весь сервис: восемь одновременно открытых карточек
// выбирают его целиком, и девятый человек получает отказ, не сделав ни одного
// запроса. Норма при этом требует обратного — бюджет считается по адресу
// спрашивающего.
//
// Доверие здесь той же природы, что и к `Remote-User`, и той же ширины: адрес
// берётся из пересылаемого заголовка, а верить пересылаемому можно ровно
// потому, что до нас дотянулся доверенный пир. Отсюда требование к контуру,
// записанное в модели угроз: прокси обязан `X-Forwarded-For` **перезаписывать**,
// а не дописывать к присланному, — иначе спрашивающий назначает себе ключ
// счётчика сам и обходит ограничитель, меняя значение.
//
// Барьером узнавания этот заголовок не служит и служить не может: кто пришёл,
// по-прежнему решает адрес самого соединения.
func ApplyTrustedProxyHeaders(app core.App) error {
settings := app.Settings()
settings.TrustedProxy.Headers = []string{"X-Forwarded-For"}
// Пустой заголовок означает, что прокси адреса не назвал; счётчик тогда
// падает обратно на адрес пира — то есть на общий, как было. Лучше общий,
// чем один пустой ключ на всех.
settings.TrustedProxy.UseLeftmostIP = true
if err := app.Save(settings); err != nil {
return fmt.Errorf("failed to apply trusted proxy headers: %w", err)
}
return nil
}