milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
//!
|
||||
//! The route is rate-limit exempt (a long-lived stream must not drain its
|
||||
//! address's token bucket) but pays the per-address SSE connection cap, which
|
||||
//! binds loopback too: hub slots are a fixed resource.
|
||||
//! binds loopback too: hub slots are a fixed resource. The address it keys on is
|
||||
//! `client_addr`, so behind a trusted proxy each remote client holds its own
|
||||
//! budget rather than all of them sharing the proxy's.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -79,12 +81,15 @@ pub fn stream(
|
||||
const hub = state.hub orelse
|
||||
return http_util.respondError(request, .service_unavailable, "live stream unavailable");
|
||||
|
||||
const peer = address.NetAddress.fromIp(request.peer);
|
||||
// The effective client, not the socket peer: behind a trusted proxy every
|
||||
// stream would otherwise share one address's budget. Acquire and release
|
||||
// read the same value, so a release can never miss the slot it took.
|
||||
const client = address.NetAddress.fromIp(request.client_addr);
|
||||
if (state.limiter) |limiter| {
|
||||
if (!limiter.tryAcquireSse(io, std.Io.Clock.awake.now(io), peer))
|
||||
if (!limiter.tryAcquireSse(io, std.Io.Clock.awake.now(io), client))
|
||||
return http_util.respondError(request, .too_many_requests, "too many live streams from this address");
|
||||
}
|
||||
defer if (state.limiter) |limiter| limiter.releaseSse(io, peer);
|
||||
defer if (state.limiter) |limiter| limiter.releaseSse(io, client);
|
||||
|
||||
const id = hub.subscribe(io) orelse
|
||||
return http_util.respondError(request, .service_unavailable, "live stream is full");
|
||||
|
||||
@@ -213,6 +213,7 @@ const WebView = struct {
|
||||
api_rate_limit_per_min: u32,
|
||||
api_localhost_exempt: bool,
|
||||
sse_max_connections_per_ip: u16,
|
||||
trusted_proxies: []const u8,
|
||||
/// Derived, not stored: the hash itself is never serialized, and the UI
|
||||
/// still has to know whether a password is set.
|
||||
auth_enabled: bool,
|
||||
@@ -246,6 +247,7 @@ pub fn view(cfg: model.Config) View {
|
||||
.api_rate_limit_per_min = cfg.web.api_rate_limit_per_min,
|
||||
.api_localhost_exempt = cfg.web.api_localhost_exempt,
|
||||
.sse_max_connections_per_ip = cfg.web.sse_max_connections_per_ip,
|
||||
.trusted_proxies = cfg.web.trusted_proxies,
|
||||
.auth_enabled = auth.authEnabled(cfg.web),
|
||||
},
|
||||
.doh_server = cfg.doh_server,
|
||||
|
||||
@@ -88,7 +88,10 @@ fn testEntry(url: []const u8, enabled: bool) pool_mod.Entry {
|
||||
}
|
||||
|
||||
fn testPool(entries: []pool_mod.Entry) pool_mod.Pool {
|
||||
return .init(entries, .{}, .{ .raw = .fromMilliseconds(50), .clock = .awake }, 1);
|
||||
return .init(entries, .{}, .{
|
||||
.attempt = .{ .raw = .fromMilliseconds(50), .clock = .awake },
|
||||
.total = .{ .raw = .fromMilliseconds(100), .clock = .awake },
|
||||
}, 1);
|
||||
}
|
||||
|
||||
test "every upstream is copied, counted and owned by the arena" {
|
||||
|
||||
Reference in New Issue
Block a user