mirror of
https://github.com/Twigpine/node.git
synced 2026-11-09 18:08:02 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d30136efdd | ||
|
|
1544bedea7 | ||
|
|
15e017ad77 |
No files matched your search
@@ -347,6 +347,14 @@ GITLAWB_TASK_READ_RATE_LIMIT=1200
|
||||
# brake caps a single-source flood. Uses GITLAWB_TRUSTED_PROXY below to resolve
|
||||
# the client IP. 0 disables. Default 120.
|
||||
GITLAWB_CREATE_RATE_LIMIT=120
|
||||
# Per-DID creation requests allowed inside the window below (repo create,
|
||||
# issue create, PR create, register, fork). 0 is rejected at startup: it would
|
||||
# silently disable the limiter. Default 10.
|
||||
GITLAWB_RATE_LIMIT_MAX=10
|
||||
# Window the per-DID creation count above applies over, in seconds. 0 is
|
||||
# rejected at startup: every timestamp would prune immediately, disabling the
|
||||
# limiter. Default 3600.
|
||||
GITLAWB_RATE_LIMIT_WINDOW_SECS=3600
|
||||
|
||||
# ── Peer-sync rate limiting (per client IP, uses GITLAWB_TRUSTED_PROXY below) ─
|
||||
# /api/v1/peers/announce and /api/v1/sync/notify accept unsigned requests from
|
||||
|
||||
@@ -244,6 +244,29 @@ pub struct Config {
|
||||
#[arg(long, env = "GITLAWB_SHUTDOWN_GRACE_SECS", default_value_t = 30)]
|
||||
pub shutdown_grace_secs: u64,
|
||||
|
||||
/// Maximum number of creation requests (repo create, issue create, PR
|
||||
/// create, register, fork) allowed per DID within the rate-limit window.
|
||||
/// 0 is rejected at parse time: it would silently disable the limiter
|
||||
/// (RateLimiter treats max_requests == 0 as allow-all), not block every
|
||||
/// creation request.
|
||||
#[arg(
|
||||
long,
|
||||
env = "GITLAWB_RATE_LIMIT_MAX",
|
||||
default_value_t = 10,
|
||||
value_parser = clap::value_parser!(u32).range(1..)
|
||||
)]
|
||||
pub rate_limit_max_requests: u32,
|
||||
|
||||
/// Rate-limit window size, in seconds, over which
|
||||
/// `rate_limit_max_requests` applies. 0 is rejected at parse time: every
|
||||
/// timestamp would prune immediately, silently disabling the limiter.
|
||||
#[arg(
|
||||
long,
|
||||
env = "GITLAWB_RATE_LIMIT_WINDOW_SECS",
|
||||
default_value_t = 3600,
|
||||
value_parser = clap::value_parser!(u64).range(1..)
|
||||
)]
|
||||
pub rate_limit_window_secs: u64,
|
||||
/// Maximum wall-clock time a single served git operation (upload-pack /
|
||||
/// receive-pack through `run_git_service`) may run before it is aborted and
|
||||
/// its process group torn down, in seconds. Bounds a git that neither
|
||||
|
||||
@@ -356,9 +356,14 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
// Per-DID limiter for the creation endpoints. Keyed on the authenticated
|
||||
// DID (attacker-varied), so bound its key set to cap memory.
|
||||
let rate_limiter =
|
||||
rate_limit::RateLimiter::new_bounded(10, std::time::Duration::from_secs(3600), 200_000);
|
||||
// DID (attacker-varied), so bound its key set to cap memory. Max and window
|
||||
// are operator-configurable via GITLAWB_RATE_LIMIT_MAX /
|
||||
// GITLAWB_RATE_LIMIT_WINDOW_SECS; both reject 0 at parse time.
|
||||
let rate_limiter = rate_limit::RateLimiter::new_bounded(
|
||||
config.rate_limit_max_requests as usize,
|
||||
std::time::Duration::from_secs(config.rate_limit_window_secs),
|
||||
200_000,
|
||||
);
|
||||
|
||||
// Per-client-IP flood brake for the creation endpoints. The per-DID limiter
|
||||
// above is bypassed by a DID farm (one throwaway did:key per repo), which is
|
||||
|
||||
@@ -70,6 +70,14 @@ impl RateLimiter {
|
||||
self.window.min(Duration::from_secs(1))
|
||||
}
|
||||
|
||||
/// The configured window, exposed so a 429 can carry an honest
|
||||
/// `retry-after`: a hardcoded value lies when the operator retunes the
|
||||
/// window, and a client that honors the header would retry into another
|
||||
/// 429 until the real window elapses.
|
||||
pub fn window(&self) -> Duration {
|
||||
self.window
|
||||
}
|
||||
|
||||
pub(crate) async fn check(&self, key: &str) -> bool {
|
||||
// max_requests == 0 means the limiter is disabled, not "block all".
|
||||
if self.max_requests == 0 {
|
||||
@@ -293,12 +301,7 @@ pub async fn rate_limit_by_did(request: Request, next: Next) -> Response {
|
||||
|
||||
if let (Some(limiter), Some(did)) = (limiter, did) {
|
||||
if !limiter.check(&did).await {
|
||||
return (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
[("retry-after", "60")],
|
||||
RATE_LIMIT_MESSAGE,
|
||||
)
|
||||
.into_response();
|
||||
return too_many_requests(limiter.window().as_secs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,10 +412,10 @@ pub const RATE_LIMIT_MESSAGE: &str = "rate limit exceeded — try again later";
|
||||
/// The shared 429 response for the per-IP flood brakes. Route-agnostic: this
|
||||
/// middleware now serves the push path AND the peer-sync routes, so the message
|
||||
/// stays generic (the offending path is recorded in the warn log below).
|
||||
pub fn too_many_requests() -> Response {
|
||||
pub fn too_many_requests(retry_after_secs: u64) -> Response {
|
||||
(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
[("retry-after", "60")],
|
||||
[("retry-after", retry_after_secs.to_string())],
|
||||
RATE_LIMIT_MESSAGE,
|
||||
)
|
||||
.into_response()
|
||||
@@ -481,7 +484,7 @@ pub async fn rate_limit_by_ip(request: Request, next: Next) -> Response {
|
||||
if let Some(key) = client_key(request.headers(), peer, limiter.trust) {
|
||||
if !limiter.limiter.check(&key).await {
|
||||
tracing::warn!(key = %key, path = %request.uri().path(), "per-IP rate limit exceeded");
|
||||
return too_many_requests();
|
||||
return too_many_requests(limiter.limiter.window().as_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user