// Per-route options: the settings go-zero's `RouteOption`s attach to a route
// rather than to the whole service.
//
// A moonzero route carries them as `hoops` — moonapi's per-route middleware — so
// the check runs before the handler and short-circuits with the status the option
// promises. Nothing here depends on the engine: the options are applied where the
// route is registered, which is the same place go-zero's `AddRoute` applies them.
///|
/// JWT settings attached to a route: the current secret and, during a rotation,
/// the previous one.
pub(all) struct JwtRoute {
enabled : Bool
secret : String
prev_secret : String?
}
///|
/// JWT settings that authenticate nothing.
pub let no_jwt : JwtRoute = { enabled: false, secret: "", prev_secret: None, }
///|
/// The per-route settings. Every one of them defaults to "inherit": no timeout, no
/// body cap, no JWT, no signature, ordinary priority, no SSE.
pub(all) struct RouteOptions {
/// A path prefix joined onto the group's own, so one registration can nest.
prefix : String
jwt : JwtRoute
signature : @handler.SignatureVerifier?
/// A per-route deadline in milliseconds; `0` inherits the service timeout.
timeout_ms : Int64
/// A per-route `Content-Length` cap; `0` inherits `RestConf.MaxBytes`.
max_bytes : Int32
/// The shedder this route draws on instead of the ordinary one, when the
/// service runs a second, higher CPU threshold for priority traffic.
priority_shedder : @shedder.Shedder?
/// Stamp `text/event-stream` on the response.
sse : Bool
clock : @clock.Clock
}
///|
/// Options that change nothing: no prefix, no auth, no deadline, no cap.
pub fn RouteOptions::new(
clock~ : @clock.Clock = @clock.Clock::system(),
) -> RouteOptions {
{
prefix: "",
jwt: no_jwt,
signature: None,
timeout_ms: 0L,
max_bytes: 0,
priority_shedder: None,
sse: false,
clock,
}
}
///|
/// Join `group` onto this option set's prefix, so a sub-group inherits the parent
/// prefix as well as its own.
pub fn RouteOptions::under(
self : RouteOptions,
prefix : String,
) -> RouteOptions {
{
prefix: join_prefix(self.prefix, prefix),
jwt: self.jwt,
signature: self.signature,
timeout_ms: self.timeout_ms,
max_bytes: self.max_bytes,
priority_shedder: self.priority_shedder,
sse: self.sse,
clock: self.clock,
}
}
///|
/// Require a valid `Authorization: Bearer ` on this route. `prev_secret` is
/// the secret a rotation is retiring: a token signed with it is still accepted
/// until the rotation window closes, which is what lets clients refresh without a
/// coordinated deploy.
pub fn RouteOptions::jwt(
self : RouteOptions,
secret~ : String,
prev_secret? : String,
) -> RouteOptions {
{
prefix: self.prefix,
jwt: { enabled: true, secret, prev_secret, },
signature: self.signature,
timeout_ms: self.timeout_ms,
max_bytes: self.max_bytes,
priority_shedder: self.priority_shedder,
sse: self.sse,
clock: self.clock,
}
}
///|
/// Require a verified `X-Content-Security` signature on this route.
pub fn RouteOptions::signature(
self : RouteOptions,
verifier : @handler.SignatureVerifier,
) -> RouteOptions {
{
prefix: self.prefix,
jwt: self.jwt,
signature: Some(verifier),
timeout_ms: self.timeout_ms,
max_bytes: self.max_bytes,
priority_shedder: self.priority_shedder,
sse: self.sse,
clock: self.clock,
}
}
///|
/// Give this route its own deadline in milliseconds.
pub fn RouteOptions::timeout(self : RouteOptions, ms : Int64) -> RouteOptions {
{
prefix: self.prefix,
jwt: self.jwt,
signature: self.signature,
timeout_ms: ms,
max_bytes: self.max_bytes,
priority_shedder: self.priority_shedder,
sse: self.sse,
clock: self.clock,
}
}
///|
/// Give this route its own `Content-Length` cap in bytes.
pub fn RouteOptions::max_bytes(
self : RouteOptions,
bytes : Int32,
) -> RouteOptions {
{
prefix: self.prefix,
jwt: self.jwt,
signature: self.signature,
timeout_ms: self.timeout_ms,
max_bytes: bytes,
priority_shedder: self.priority_shedder,
sse: self.sse,
clock: self.clock,
}
}
///|
/// Draw this route from the priority shedder, so a CPU spike sheds ordinary
/// traffic before it touches these calls.
pub fn RouteOptions::priority(
self : RouteOptions,
shedder : @shedder.Shedder,
) -> RouteOptions {
{
prefix: self.prefix,
jwt: self.jwt,
signature: self.signature,
timeout_ms: self.timeout_ms,
max_bytes: self.max_bytes,
priority_shedder: Some(shedder),
sse: self.sse,
clock: self.clock,
}
}
///|
/// Mark this route as a server-sent-event stream: the response carries the SSE
/// media type, no caching, and a keep-alive connection.
pub fn RouteOptions::sse(self : RouteOptions) -> RouteOptions {
{
prefix: self.prefix,
jwt: self.jwt,
signature: self.signature,
timeout_ms: self.timeout_ms,
max_bytes: self.max_bytes,
priority_shedder: self.priority_shedder,
sse: true,
clock: self.clock,
}
}
///|
/// Join a prefix with a path into a single, normalised path: at most one slash
/// between the two, a guaranteed leading slash, and `"/"` for the empty result.
pub fn join_path(prefix : String, path : String) -> String {
let mut end = prefix.length()
while end > 0 && prefix[end - 1] == '/' {
end = end - 1
}
let base = prefix[0:end].to_owned()
let head = if base.length() == 0 || base[0] == '/' {
base
} else {
"/" + base
}
if path.length() == 0 {
return if head.length() == 0 { "/" } else { head }
}
let tail = if path[0] == '/' { path } else { "/" + path }
head + tail
}
///|
/// `join_path` for a prefix that may be absent: two empty prefixes stay empty
/// rather than collapsing to the root path, so a group with no prefix of its own
/// adds nothing to a route.
pub fn join_prefix(prefix : String, path : String) -> String {
match join_path(prefix, path) {
"/" => ""
other => other
}
}