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 }