web.mw
web 中间件框架自带件:会话族(with_sessions/grant/drop)+ flash 读即清 + log_requests + timeout_ms(Plan 2b Task 1;设计 docs/superpowers/specs/2026-09-27-web-framework-design.md §4.1/§4.5/§7;纪律 §14-1)
收编口径(不复制):签名会话令牌与 Set-Cookie 行构造全走 http.frm.auth 现成面 —— auth_tok_issue/auth_tok_verify(签发/验签,恒时比较+过期域 frm 内铸)、 auth_cookie_set(注入面封口的 Set-Cookie 行构造:CR/LF/分号/DQUOTE 出域即拒)。 本模块零 crypto、零手工 cookie 拼接。auth_cookie_get 未收:web.core Req.cookies 已在 req_full 解析 Cookie 头(cookie 访问器直读),重复解析无增益。
时钟注入(§8-A5 可测性;测试传 fake,真钟归装配):
- with_sessions(key, now) 第二参 = 时钟 fn() -> I64(2b Task 3 serve 以
| | now_ms() / 1000 装配);
- grant_session 第 5 参 now —— spec §4.4 链式示例为 4 参,系伪码口径;
exp = now+ttl 需真值,§8-A5 now 经参数注入条款优先(名字面不变,记录于 task-1-report)。
- 单位约定:now/ttl/时钟恒 unix 秒(todo_app 验收口径;auth 令牌 exp 同单位),
Max-Age 恒秒(RFC 6265)——serve 侧装配须 /1000,勿直喂 now_ms()。
会话机制(§4.1/§4.3;方案 (a) 最小改动):Req 增 session: List[Pair] 槽 (core.ct;params 同款),with_sessions 验签后经 req_set_session 回填, handler 侧 §4.3 冻结名 session(r, "uid") 读取(缺回空)。验签失败(无 cookie/形坏/签名不合/过期)一律原样放行(session 槽空)——拦截属应用守卫 中间件(need_login 形),with_sessions 只管注入,不越权代拦。
flash 机制(读即清最小形态,记录):载体 cookie web_flash=<penc msg>
(会话 cookie,无 Max-Age)。Req 是值、读侧无法就地清——清除挂在
with_sessions 响应侧:入请求携 web_flash 且响应未再写 flash(防 PRG
写后自吞)时自动补 web_flash=; Max-Age=0 清除行。不经 with_sessions
的路由 flash 不自清(登记边界;req_flash 单独可读)。
timeout_ms 诚实口径:返回透传中间件(上限已验,n<=0 装配即 panic); 真超时挂并发波(§8.2 v1 阻塞串行档,阻塞调用无中断点)——校验语义面, 并发 RT 落地后在此闭包内换真实现,调用点零改(§4.5)。
use 面:web.core(Resp/Req 取参族)+ http.frm.auth(收编面)+ std.str (starts_with);std.crypto 不直 use(auth 单叶,消费方再入即同叶双径 E5020)。
pub struct
struct Pair
| Field | Type |
|---|---|
k |
Str |
v |
Str |
struct Resp
| Field | Type |
|---|---|
status |
I32 |
headers |
List[Pair] |
body |
Str |
struct JObj
| Field | Type |
|---|---|
parts |
List[Str] |
struct Req[S]
| Field | Type |
|---|---|
method |
Str |
path |
Str |
query |
Str |
body |
Str |
headers |
List[Pair] |
cookies |
List[Pair] |
params |
List[Pair] |
session |
List[Pair] |
state |
S |
struct RouteSeg
| Field | Type |
|---|---|
kind |
Str |
text |
Str |
struct Route[S]
| Field | Type |
|---|---|
m |
Str |
segs |
List[RouteSeg] |
chain |
List[fn(&Req[S], fn(&Req[S]) -> Resp) -> Resp] |
h |
fn(&Req[S]) -> Resp |
struct Router[S]
| Field | Type |
|---|---|
state |
S |
routes |
List[Route[S]] |
mws |
List[fn(&Req[S], fn(&Req[S]) -> Resp) -> Resp] |
not_found_h |
List[fn(&Req[S]) -> Resp] |
not_allowed_h |
List[fn(&Req[S]) -> Resp] |
struct Match[S]
| Field | Type |
|---|---|
kind |
I32 |
route |
Route[S] |
params |
List[Pair] |
splat |
Str |
allow |
Str |
struct AuthTok
| Field | Type |
|---|---|
ok |
I64 |
why |
I64 |
tok |
Str |
struct TokVer
| Field | Type |
|---|---|
rc |
I64 |
why |
I64 |
uid |
Str |
struct AuthClaim
| Field | Type |
|---|---|
k |
I64 |
v |
I64 |
struct JwtVer
| Field | Type |
|---|---|
rc |
I64 |
why |
I64 |
struct SessStore
| Field | Type |
|---|---|
n |
I64 |
e0 |
I64 |
e1 |
I64 |
e2 |
I64 |
e3 |
I64 |
e4 |
I64 |
e5 |
I64 |
e6 |
I64 |
e7 |
I64 |
s0 |
Str |
s1 |
Str |
s2 |
Str |
s3 |
Str |
s4 |
Str |
s5 |
Str |
s6 |
Str |
s7 |
Str |
r0 |
Str |
r1 |
Str |
r2 |
Str |
r3 |
Str |
r4 |
Str |
r5 |
Str |
r6 |
Str |
r7 |
Str |
struct SessPut
| Field | Type |
|---|---|
ok |
I64 |
why |
I64 |
st |
SessStore |
struct SessGet
| Field | Type |
|---|---|
rc |
I64 |
user |
Str |
pub fn
| Signature | Returns | Description |
|---|---|---|
grant_session(r: Resp, key: Str, uid: Str, ttl: I64, now: I64) |
Resp |
签发:Set-Cookie sid=<tok>(HttpOnly+SameSite=Lax 随,frm/auth 同款 §7-3; Max-Age=ttl 秒)。uid 出域(frm 构造拒)→ 原样返回不签发。 |
drop_session(r: Resp, key: Str) |
Resp |
注销:Max-Age=0 即刻删除面(frm/auth 口径)。key 形参留置:服务端吊销面 (存储对偶)属志向,当前无状态令牌注销 = 客户端删 cookie。 |
with_sessions[S](key: Str, now: fn() -> I64) |
fn(&Req[S], fn(&Req[S]) -> Resp) -> Resp |
sid cookie → 验签 → 注入 session 槽;失败原样放行(见头注)。响应侧顺带 执行 flash 读即清(flash_sweep)。 |
flash(r: Resp, msg: Str) |
Resp |
写:cookie web_flash=<penc msg>(会话 cookie,无 Max-Age;值经百分号编码出 frm cookie-octet 域雷区,读侧 pdec 还原——core UTF-8 安全解码) |
req_flash[S](r: &Req[S]) |
Str |
读(§4.3 冻结名 req.flash()):读即清的「读」半边,清半边在 with_sessions 响应侧(头注 flash 机制)。缺回空。 |
log_requests[S](r: &Req[S], next: fn(&Req[S]) -> Resp) |
Resp |
访问日志:next 后落一行(此时才有状态可印;§4.5 形态 ① 纯函数)。 行形 "GET /x 200";行缓冲/逐条刷盘政策归 serve(Task 3),本件只 println。 |
timeout_ms[S](ms: I64) |
fn(&Req[S], fn(&Req[S]) -> Resp) -> Resp |
组级超时档位(§4.5/§8.1):n<=0 装配即 panic(失败在启动,不在半夜); 现阶段透传(头注诚实口径),真实现随并发波在闭包内换血。 |
html(body: Str) |
Resp |
— |
json(body: Str) |
Resp |
— |
text(body: Str) |
Resp |
— |
redirect(loc: Str) |
Resp |
— |
status(n: I32, body: Str) |
Resp |
— |
xml(body: Str) |
Resp |
— |
yaml(body: Str) |
Resp |
— |
err_json(n: I32, msg: Str) |
Resp |
— |
attachment(r: Resp, filename: Str) |
Resp |
— |
with(r: Resp, k: Str, v: Str) |
Resp |
— |
with_status(r: Resp, n: I32) |
Resp |
— |
resp_header(r: Resp, k: Str) |
Str |
— |
resp_body(r: Resp) |
Str |
— |
resp_status(r: Resp) |
I32 |
— |
json_obj() |
JObj |
— |
str(b: JObj, k: Str, v: Str) |
JObj |
— |
i64(b: JObj, k: Str, v: I64) |
JObj |
— |
bool(b: JObj, k: Str, v: Bool) |
JObj |
— |
strs(b: JObj, k: Str, xs: List[Str]) |
JObj |
— |
end(b: JObj) |
Str |
— |
end_status(b: JObj, n: I32) |
Resp |
— |
csv_rows(headers: List[Str], rows: List[List[Str]]) |
Str |
— |
req_of[S](method: Str, path: Str, state: S) |
Req[S] |
— |
req_full[S](method: Str, path: Str, body: Str, hdrs: List[Pair], state: S) |
Req[S] |
— |
req_set_params[S](r: &Req[S], params: List[Pair]) |
Req[S] |
— |
req_set_session[S](r: &Req[S], sess: List[Pair]) |
Req[S] |
— |
pdec(s: Str) |
Str |
— |
param[S](r: &Req[S], name: Str) |
Str |
— |
pair_get(ps: List[Pair], k: Str) |
Str |
— |
param_i64[S](r: &Req[S], name: Str) |
Option[I64] |
— |
param_f64[S](r: &Req[S], name: Str) |
Option[F64] |
— |
query[S](r: &Req[S], name: Str) |
Str |
— |
query_all[S](r: &Req[S], name: Str) |
List[Str] |
— |
form[S](r: &Req[S], name: Str) |
Str |
— |
form_all[S](r: &Req[S], name: Str) |
List[Str] |
— |
header[S](r: &Req[S], name: Str) |
Str |
— |
cookie[S](r: &Req[S], name: Str) |
Str |
— |
session[S](r: &Req[S], name: Str) |
Str |
— |
router[S](state: S) |
Router[S] |
— |
get[S](r: Router[S], p: Str, h: fn(&Req[S]) -> Resp) |
Router[S] |
— |
post[S](r: Router[S], p: Str, h: fn(&Req[S]) -> Resp) |
Router[S] |
— |
put[S](r: Router[S], p: Str, h: fn(&Req[S]) -> Resp) |
Router[S] |
— |
middleware[S](r: Router[S], mw: fn(&Req[S], fn(&Req[S]) -> Resp) -> Resp) |
Router[S] |
— |
mount_at[S](r: Router[S], prefix: Str, sub: Router[S]) |
Router[S] |
— |
has_conflict[S](r: Router[S]) |
Bool |
— |
install[S](r: Router[S], p: fn(Router[S]) -> Router[S]) |
Router[S] |
— |
not_found[S](r: Router[S], h: fn(&Req[S]) -> Resp) |
Router[S] |
— |
method_not_allowed[S](r: Router[S], h: fn(&Req[S]) -> Resp) |
Router[S] |
— |
test_call[S](r: Router[S], method: Str, path: Str, body: Str) |
Resp |
— |
route_match[S](r: Router[S], m: Str, path: Str) |
Match[S] |
— |
dispatch_h[S](r: Router[S], method: Str, path: Str, body: Str, hdrs: List[Pair]) |
Resp |
— |
dispatch[S](r: Router[S], method: Str, path: Str, body: Str) |
Resp |
— |
auth_err_ok() |
I64 |
— |
auth_err_empty() |
I64 |
— |
auth_err_shape() |
I64 |
— |
auth_err_uid() |
I64 |
— |
auth_err_expnum() |
I64 |
— |
auth_err_sigform() |
I64 |
— |
auth_err_sig() |
I64 |
— |
auth_err_expired() |
I64 |
— |
auth_err_b64() |
I64 |
— |
auth_err_alg() |
I64 |
— |
auth_err_nbf() |
I64 |
— |
auth_err_iat() |
I64 |
— |
auth_err_claim() |
I64 |
— |
auth_err_full() |
I64 |
— |
auth_cookie_get(cookie_hdr: Str, name: Str) |
Str |
— |
auth_cookie_set(name: Str, value: Str, maxage: I64, httponly: I64) |
Str |
— |
at_ok(t: AuthTok) |
I64 |
— |
at_tok(t: AuthTok) |
Str |
— |
auth_tok_issue(key: Str, uid: Str, now: I64, ttl: I64) |
AuthTok |
— |
tv_rc(v: TokVer) |
I64 |
— |
tv_why(v: TokVer) |
I64 |
— |
tv_uid(v: TokVer) |
Str |
— |
auth_tok_verify(key: Str, tok: Str, now: I64) |
TokVer |
— |
auth_b64url_encode(src: Str) |
Str |
— |
auth_jwt_hdr() |
Str |
— |
ac_k(c: AuthClaim) |
I64 |
— |
ac_v(c: AuthClaim) |
I64 |
— |
auth_jwt_claim(payload: Str, name: Str) |
AuthClaim |
— |
auth_jwt_sign(key: Str, payload: Str) |
AuthTok |
— |
auth_jwt_hdr_b64() |
Str |
— |
jwt_rc(v: JwtVer) |
I64 |
— |
jwt_why(v: JwtVer) |
I64 |
— |
auth_jwt_verify(key: Str, tok: Str, now: I64) |
JwtVer |
— |
auth_sess_cap() |
I64 |
— |
auth_sess_new() |
SessStore |
— |
sp_ok(p: SessPut) |
I64 |
— |
sp_why(p: SessPut) |
I64 |
— |
sp_st(p: SessPut) |
SessStore |
— |
auth_sess_put(t: SessStore, sid: Str, user: Str, now: I64, ttl: I64) |
SessPut |
— |
sg_rc(g: SessGet) |
I64 |
— |
sg_user(g: SessGet) |
Str |
— |
auth_sess_get(t: SessStore, sid: Str, now: I64) |
SessGet |
— |
auth_sess_del(t: SessStore, sid: Str) |
SessStore |
— |
auth_sess_gc(t: SessStore, now: I64) |
SessStore |
— |
auth_sess_len(t: SessStore) |
I64 |
— |