From 8135d57483b3c83fee3859e5b3c94f9ee4602d2a Mon Sep 17 00:00:00 2001
From: taadis
Date: Thu, 22 Oct 2026 19:47:33 +0800
Subject: [PATCH] add HttpBearerAuth middleware, demo, and tests
---
README.md | 13 +-
README.zh.md | 13 +-
fun/demos/http-bearer-demo/README.md | 77 +++++
fun/demos/http-bearer-demo/package.fun | 15 +
fun/demos/http-bearer-demo/src/main.fun | 31 ++
fun/demos/http-bearer-demo/web/index.html | 57 ++++
fun/lib/http-bearer.fun | 172 ++++++++++++
.../expected/lib-http-bearer-test.fun.txt | 36 +++
fun/test/expected/lib-loadcheck.sh.txt | 3 +-
fun/test/expected/macos/lib-loadcheck.sh.txt | 3 +-
fun/test/lib-http-bearer-test.fun | 264 ++++++++++++++++++
fun/test/regression-list.txt | 1 +
fun/test/run-windows-regression.bat | 2 +-
13 files changed, 672 insertions(+), 15 deletions(-)
create mode 100644 fun/demos/http-bearer-demo/README.md
create mode 100644 fun/demos/http-bearer-demo/package.fun
create mode 100644 fun/demos/http-bearer-demo/src/main.fun
create mode 100644 fun/demos/http-bearer-demo/web/index.html
create mode 100644 fun/lib/http-bearer.fun
create mode 100644 fun/test/expected/lib-http-bearer-test.fun.txt
create mode 100644 fun/test/lib-http-bearer-test.fun
diff --git a/README.md b/README.md
index 48d2d84..2d43dd1 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@
In one sentence: **Fun is a scripting language that goes all the way from JSON to machine code.**
- One runtime spans the whole spectrum, from high-level data work (JSON/FD) down to low-level system programming (FFI, C at runtime, JIT, machine code);
-- 65 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`;
+- 66 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`;
- in AI-agent and domestic-IT scenarios, it is the lightweight glue connecting OSes, databases, chips, and business logic.
Fun is designed around a simple premise: **data and code should be equally first-class, and a script should be able to reach down to the machine without leaving the language.**
@@ -70,7 +70,7 @@ Fun is written in Pascal (Delphi / Free Pascal). The core is a small tree-walkin
- **Keyword aliasing** — a canonical English keyword set with pluggable alias packs (Chinese, French, custom) selected at runtime (see [below](#keyword-aliasing)).
- **Cross-platform** — Windows (32/64-bit), Linux, ARM, and Windows CE.
- **Dual distribution** — command-line `fun.exe` and embeddable runtime `fun.dll` exporting a simple `Run` interface.
-- **Self-hosted standard library** — 65 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.
+- **Self-hosted standard library** — 66 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.
---
@@ -372,7 +372,7 @@ fun/
│ ├── utils/ # Utility functions
│ └── prj/fun/ # Projects & build scripts (funcmd.dpr, Delphi/FPC)
├── fun/
-│ ├── lib/ # Standard library (pure Fun, 65 modules)
+│ ├── lib/ # Standard library (pure Fun, 66 modules)
│ ├── demo/ # Language syntax demos
│ ├── demos/ # Full applications & benchmarks
│ └── key_*.ini # Keyword alias packs (CN / FR / PUA)
@@ -497,16 +497,16 @@ win64/aarch64 target, so there is nothing to build with.
## Standard library
-The standard library in [`fun/lib`](fun/lib) is written in pure Fun (65 modules):
+The standard library in [`fun/lib`](fun/lib) is written in pure Fun (66 modules):
-> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 65 modules written in Fun itself.
+> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 66 modules written in Fun itself.
- **Data**: `lib-json`, `lib-yaml`, `lib-xml`, `lib-base64`, `lib-cstruct`, `lib-md5`, `lib-crypt`
- **Utility**: `uuid`
- **Collections / algorithms**: `lib-set`, `lib-tree`, `lib-stack`, `lib-dyns`, `lib-math`
- **Text**: `lib-string`, `lib-regex`, `lib-match`, `lib-unicode`
- **System / IO**: `lib-file`, `lib-os`, `lib-host`, `lib-time`, `lib-cmdline`, `lib-proc`
-- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `http-cors`
+- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `http-bearer`, `http-cors`
- **Databases**: `lib-orm`, `lib-orm-pro`, `lib-orm-gen`, `lib-orm-rpc`, `lib-orm-cte`, `lib-ado`, `lib-ado-schema`
- **Native / low-level**: `lib-winapi`, `lib-winole`, `lib-tcc` (C compile), `lib-jit`, `lib-asm`, `lib-asm-pro`
- **UI**: `lib-ui`, `lib-ui-base`, `lib-dialog`, `lib-trayicon`
@@ -532,6 +532,7 @@ The standard library in [`fun/lib`](fun/lib) is written in pure Fun (65 modules)
- [`fun/demos/package-demo`](fun/demos/package-demo) — a small package with a path dependency: `tidy`, `run` (project and flat mode), and `pack`.
- [`fun/demos/test-demo`](fun/demos/test-demo) — a lib-only package with tests, a shared helper, and a failing-assertion example.
- [`fun/demos/http-cors-demo`](fun/demos/http-cors-demo) — CORS preflight, credentials, exposed headers, and a browser page.
+- [`fun/demos/http-bearer-demo`](fun/demos/http-bearer-demo) — Bearer token auth, 401 challenges, and a same-origin page.
---
diff --git a/README.zh.md b/README.zh.md
index d20434d..29f8184 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -39,7 +39,7 @@
一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。**
- 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系;
-- 自带 65 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
+- 自带 66 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
- 在信创与 AI Agent 场景中,作为连接国产 OS、数据库、芯片与业务逻辑的轻量级胶水。
Fun 的设计出发点很简单:**数据与代码都应该是头等公民,而且一段脚本应该能在不离开语言的情况下一路触达机器底层。**
@@ -70,7 +70,7 @@ Fun 使用 Pascal(Delphi / Free Pascal)编写。核心是一个小巧的树
- **关键字别名** —— 规范英文关键字 + 可插拔的别名包(中文、法文、自定义),运行时选择(见[下文](#关键字别名))。
- **跨平台** —— Windows(32/64 位)、Linux、ARM 与 Windows CE。
- **双形态分发** —— 命令行 `fun.exe` 与嵌入运行时 `fun.dll`(导出简单的 `Run` 接口)。
-- **自托管的标准库** —— 65 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。
+- **自托管的标准库** —— 66 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。
---
@@ -365,7 +365,7 @@ fun/
│ ├── utils/ # 工具函数
│ └── prj/fun/ # 工程与构建脚本(funcmd.dpr,Delphi/FPC)
├── fun/
-│ ├── lib/ # 标准库(纯 Fun 编写,65 个模块)
+│ ├── lib/ # 标准库(纯 Fun 编写,66 个模块)
│ ├── demo/ # 语言语法演示
│ ├── demos/ # 完整应用与基准测试
│ └── key_*.ini # 关键字别名包(CN / FR / PUA)
@@ -477,16 +477,16 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala
## 标准库
-标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(65 个模块):
+标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(66 个模块):
-> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——65 个模块全部用 Fun 自身写成。
+> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——66 个模块全部用 Fun 自身写成。
- **数据**:`lib-json`、`lib-yaml`、`lib-xml`、`lib-base64`、`lib-cstruct`、`lib-md5`、`lib-crypt`
- **工具**:`uuid`
- **集合 / 算法**:`lib-set`、`lib-tree`、`lib-stack`、`lib-dyns`、`lib-math`
- **文本**:`lib-string`、`lib-regex`、`lib-match`、`lib-unicode`
- **系统 / IO**:`lib-file`、`lib-os`、`lib-host`、`lib-time`、`lib-cmdline`、`lib-proc`
-- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`http-cors`
+- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`http-bearer`、`http-cors`
- **数据库**:`lib-orm`、`lib-orm-pro`、`lib-orm-gen`、`lib-orm-rpc`、`lib-orm-cte`、`lib-ado`、`lib-ado-schema`
- **原生 / 底层**:`lib-winapi`、`lib-winole`、`lib-tcc`(C 编译)、`lib-jit`、`lib-asm`、`lib-asm-pro`
- **UI**:`lib-ui`、`lib-ui-base`、`lib-dialog`、`lib-trayicon`
@@ -509,6 +509,7 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala
- [`fun/demos/package-demo`](fun/demos/package-demo) —— 带路径依赖的小项目:`tidy`、`run`(项目/平铺)与 `pack`。
- [`fun/demos/test-demo`](fun/demos/test-demo) —— 只有库的包及测试:共享 helper、通过用例与失败断言示例。
- [`fun/demos/http-cors-demo`](fun/demos/http-cors-demo) —— CORS 预检、credentials、暴露响应头与浏览器页面示例。
+- [`fun/demos/http-bearer-demo`](fun/demos/http-bearer-demo) —— Bearer token 认证、401 挑战与同源页面示例。
---
diff --git a/fun/demos/http-bearer-demo/README.md b/fun/demos/http-bearer-demo/README.md
new file mode 100644
index 0000000..eb47b16
--- /dev/null
+++ b/fun/demos/http-bearer-demo/README.md
@@ -0,0 +1,77 @@
+# http-bearer-demo
+
+A small API and page showing Bearer token authentication:
+
+- `GET /api/public` is open;
+- `GET /api/me` requires `Authorization: Bearer demo-token`;
+- missing credentials get a `Bearer realm="demo"` challenge;
+- an invalid token adds `error="invalid_token"`;
+- the token is never stored in `req.auth`.
+
+```text
+package.fun # manifest
+src/main.fun # API + static page on :8080
+web/index.html # same-origin page
+```
+
+## Run
+
+`fun` below is the command-line binary (`fun.exe` on Windows). From the
+repository root, after building, the in-tree binary is
+`src/prj/fun/funcmd`; from this directory that is
+`../../../src/prj/fun/funcmd`.
+
+```text
+cd fun/demos/http-bearer-demo
+fun run
+# or: ../../../src/prj/fun/funcmd run
+```
+
+Open . The page calls the API with the token in the
+input (default `demo-token`).
+
+## Curl
+
+Missing credentials:
+
+```text
+curl -i http://127.0.0.1:8080/api/me
+```
+
+Expected: `401` with `WWW-Authenticate: Bearer realm="demo"`.
+
+Invalid token:
+
+```text
+curl -i http://127.0.0.1:8080/api/me \
+ -H 'Authorization: Bearer wrong-token'
+```
+
+Expected: `401` with `Bearer realm="demo", error="invalid_token"`.
+
+Malformed header:
+
+```text
+curl -i http://127.0.0.1:8080/api/me \
+ -H 'Authorization: Bearer'
+```
+
+Expected: `401` with `Bearer realm="demo", error="invalid_request"`.
+
+Valid token:
+
+```text
+curl -i http://127.0.0.1:8080/api/me \
+ -H 'Authorization: Bearer demo-token'
+```
+
+Expected: `200` and `{"sub":"demo-user","scheme":"bearer"}`.
+
+## Notes
+
+- Only the `Authorization` header is read; query/form tokens are not accepted.
+- The verifier is application code. Real applications should validate an
+ opaque token against a store, or verify a JWT signature and claims.
+- Bearer tokens are secrets: use HTTPS and do not log `req.credentials`.
+- `demo-token` is fixed for demonstration only; never use it in production.
+- Basic and Bearer are separate scheme middlewares; pick one per route/group.
diff --git a/fun/demos/http-bearer-demo/package.fun b/fun/demos/http-bearer-demo/package.fun
new file mode 100644
index 0000000..c62bf7e
--- /dev/null
+++ b/fun/demos/http-bearer-demo/package.fun
@@ -0,0 +1,15 @@
+fun package()
+ result = [
+ name: 'http-bearer-demo',
+ version: '0.1.0',
+ bin: 'src/main.fun',
+ lib: '',
+ 'fun': '9.4',
+
+ dependencies: [],
+
+ // >>> generated - do not edit; run 'fun package tidy' to refresh
+ resolved: []
+ // <<< generated
+ ];
+end fun;
diff --git a/fun/demos/http-bearer-demo/src/main.fun b/fun/demos/http-bearer-demo/src/main.fun
new file mode 100644
index 0000000..9edd3b0
--- /dev/null
+++ b/fun/demos/http-bearer-demo/src/main.fun
@@ -0,0 +1,31 @@
+use '../../../lib/http-server.fun';
+use '../../../lib/http-bearer.fun';
+
+fun verifyDemo(req, c)
+ if c['token'] = 'demo-token' then
+ var p = new [];
+ p['sub'] = 'demo-user';
+ p['roles'] = new [];
+ p['roles']['reader'] = true;
+ result = p;
+ else
+ result = false;
+ end if;
+end fun;
+
+fun meH(req, res)
+ res.json([sub: req.auth['sub'], scheme: req.auth['scheme']]);
+end fun;
+
+fun publicH(req, res)
+ res.json([ok: true, from: 'public']);
+end fun;
+
+var s = HttpServer(8080);
+s.Use(HttpLogger());
+s.get('/api/public', publicH);
+var api = s.group('/api');
+api.Use(HttpBearerAuth(verifyDemo, [realm: 'demo']));
+api.get('/me', meH);
+s.static('/', 'web');
+s.run();
diff --git a/fun/demos/http-bearer-demo/web/index.html b/fun/demos/http-bearer-demo/web/index.html
new file mode 100644
index 0000000..d8249ae
--- /dev/null
+++ b/fun/demos/http-bearer-demo/web/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Fun Bearer demo
+
+
+
+ Fun Bearer demo
+
API and page share http://127.0.0.1:8080; no CORS is involved.
+
+ Token:
+
+
+
+
+ Click a button.
+
+
+
+
diff --git a/fun/lib/http-bearer.fun b/fun/lib/http-bearer.fun
new file mode 100644
index 0000000..47d8d8d
--- /dev/null
+++ b/fun/lib/http-bearer.fun
@@ -0,0 +1,172 @@
+// Copyright (c) 2010-2026 Zhang Weidong
+// SPDX-License-Identifier: MIT
+//
+// http-bearer: HTTP Bearer authentication middleware (RFC 6750).
+//
+// use 'http-bearer.fun';
+//
+// fun verifyBearer(req, c)
+// var claims = Jwt.verify(c['token']); // app code
+// if claims = nil then return false; end if
+// result = [sub: claims.sub, roles: claims.roles];
+// end fun;
+//
+// api.Use(HttpBearerAuth(verifyBearer, [realm: 'api']));
+//
+// Options: realm (default 'Restricted'), optional (true allows a missing
+// Authorization header; invalid tokens still fail).
+//
+// verify(req, credentials) returns:
+// * false / nil -> 401 (invalid_token challenge)
+// * true -> success, default principal [scheme:'bearer']
+// * a principal set -> success, stored in req.auth
+// * a set with status >= 400 -> custom failure (status/message/headers)
+//
+// status, message and headers are reserved on the verifier result; use other
+// keys for business claims. credentials is [scheme:'bearer', token:'...']; the
+// token is cleared after verification and is never stored in req.auth. Only the
+// Authorization header is read; query/form tokens are not accepted. Bearer
+// tokens are secrets: use HTTPS. Parse-only mode keeps the token in
+// req.credentials for explicit forwarding; the application must clear it.
+// Authentication only: scope/role checks stay in the handler.
+fun _bearerWs(c)
+ result = c = ' ' or c = 9.toChar();
+end fun;
+
+fun _bearerTrim(s)
+ var b = 0;
+ var e = s.length();
+ while b < e and _bearerWs(s.substr(b, 1)) do b += 1; end do;
+ while e > b and _bearerWs(s.substr(e - 1, 1)) do e -= 1; end do;
+ result = s.substr(b, e - b);
+end fun;
+
+// RFC 7235 token68: ALPHA / DIGIT / - . _ ~ + /, then optional '=' padding.
+fun _bearerTokenOk(s)
+ result = false;
+ if s = '' then return; end if;
+ var pad = false;
+ var base = 0;
+ var i = 0;
+ while i < s.length() do
+ var c = s.substr(i, 1);
+ if c = '=' then
+ pad = true;
+ elsif pad then
+ return;
+ elsif 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~+/'.subpos(c) < 0 then
+ return;
+ else
+ base += 1;
+ end if;
+ i += 1;
+ end do;
+ result = base > 0;
+end fun;
+
+// Authorization header -> credentials set, or nil when malformed
+fun _bearerParse(h)
+ result = nil;
+ if h = nil or h = '' then return; end if;
+ var sp = h.subpos(' ');
+ if sp <= 0 then return; end if;
+ if h.substr(0, sp).lower() <> 'bearer' then return; end if;
+ var t = _bearerTrim(h.substr(sp + 1));
+ if not _bearerTokenOk(t) then return; end if;
+ var r = new [];
+ r['scheme'] = 'bearer';
+ r['token'] = t;
+ result = r;
+end fun;
+
+fun _bearerRealm(opts)
+ result = 'Restricted';
+ if opts <> nil then
+ var r = opts['realm'];
+ if r <> nil and r <> '' then
+ var s = '' & r;
+ s = s.replace(13.toChar(), '').replace(10.toChar(), '');
+ s = s.replace('"', "'").replace(92.toChar(), '');
+ result = s;
+ end if;
+ end if;
+end fun;
+
+fun _bearerFail(res, code, message, headers, realm, err)
+ if code = nil or code < 400 then code = 401; end if;
+ res.status(code);
+ var hasCt = false;
+ var hasWa = false;
+ if headers <> nil then
+ for k: v in headers do
+ if k.lower() = 'content-type' then hasCt = true; end if;
+ if code = 401 and k.lower() = 'www-authenticate' then hasWa = true; end if;
+ res.header(k, v);
+ end do;
+ end if;
+ if not hasCt then
+ res.header('Content-Type', 'text/plain; charset=utf-8');
+ end if;
+ if code = 401 and not hasWa then
+ var ch = 'Bearer realm="' & realm & '"';
+ if err <> nil and err <> '' then ch &= ', error="' & err & '"'; end if;
+ res.header('WWW-Authenticate', ch);
+ end if;
+ if message = nil or message = '' then message = 'unauthorized'; end if;
+ res.send(message);
+end fun;
+
+class CBearerAuth(v, r, o)
+ var verify = v;
+ var realm = r;
+ var optional = o;
+
+ fun run(req, res, nx)
+ var h = req.header('authorization');
+ var cred = _bearerParse(h);
+ if verify = nil then
+ req.credentials = cred;
+ nx();
+ elsif cred = nil then
+ if optional and (h = nil or h = '') then
+ req.auth = nil;
+ nx();
+ elsif h = nil or h = '' then
+ _bearerFail(res, 401, 'unauthorized', nil, realm, nil);
+ else
+ _bearerFail(res, 401, 'unauthorized', nil, realm, 'invalid_request');
+ end if;
+ else
+ req.credentials = cred;
+ var r = verify(req, cred);
+ req.credentials = nil;
+ if r = false or r = nil then
+ _bearerFail(res, 401, 'unauthorized', nil, realm, 'invalid_token');
+ elsif r = true then
+ var p = new [];
+ p['scheme'] = 'bearer';
+ req.auth = p;
+ nx();
+ else
+ var st = r['status'];
+ if st <> nil and st >= 400 then
+ _bearerFail(res, st, r['message'], r['headers'], realm, 'invalid_token');
+ else
+ var p = new [];
+ for k: v in r do p[k] = v; end do;
+ if p['scheme'] = nil then p['scheme'] = 'bearer'; end if;
+ if p.@has('token') then p['token'] = nil; end if;
+ req.auth = p;
+ nx();
+ end if;
+ end if;
+ end if;
+ end fun;
+end class;
+
+fun HttpBearerAuth(verify, opts)
+ var realm = _bearerRealm(opts);
+ var optional = false;
+ if opts <> nil and opts['optional'] = true then optional = true; end if;
+ result = CBearerAuth(verify, realm, optional);
+end fun;
diff --git a/fun/test/expected/lib-http-bearer-test.fun.txt b/fun/test/expected/lib-http-bearer-test.fun.txt
new file mode 100644
index 0000000..46a0cbc
--- /dev/null
+++ b/fun/test/expected/lib-http-bearer-test.fun.txt
@@ -0,0 +1,36 @@
+bearer ok 200: True
+bearer sub: True
+bearer scheme: True
+bearer no token in auth: True
+bearer credentials cleared: True
+bearer missing 401: True
+bearer missing challenge: True
+bearer missing no error: True
+bearer wrong 401: True
+bearer invalid token error: True
+bearer malformed 401: True
+bearer invalid request error: True
+bearer bad padding 401: True
+bearer only padding 401: True
+bearer lower scheme 200: True
+bearer multiple spaces 200: True
+bearer rejects basic: True
+bearer optional anonymous: True
+bearer optional user: True
+bearer optional bad 401: True
+bearer optional malformed 401: True
+bearer parse-only empty: True
+bearer parse-only malformed: True
+bearer token68: True
+bearer bad token68 401: True
+bearer default true 200: True
+bearer default scheme: True
+bearer blocked 403: True
+bearer blocked header: True
+bearer blocked body: True
+bearer blocked no challenge: True
+bearer custom 401: True
+bearer custom challenge: True
+bearer custom body: True
+bearer strips verifier token: True
+ok
diff --git a/fun/test/expected/lib-loadcheck.sh.txt b/fun/test/expected/lib-loadcheck.sh.txt
index bfc8208..4e06b2c 100644
--- a/fun/test/expected/lib-loadcheck.sh.txt
+++ b/fun/test/expected/lib-loadcheck.sh.txt
@@ -1,4 +1,5 @@
LOAD http-auth
+LOAD http-bearer
LOAD http-client
LOAD http-cors
LOAD http-server
@@ -66,4 +67,4 @@ LOAD lib-zlib
LOAD test
LOAD uuid
-loaded=61 failed=6
\ No newline at end of file
+loaded=62 failed=6
\ No newline at end of file
diff --git a/fun/test/expected/macos/lib-loadcheck.sh.txt b/fun/test/expected/macos/lib-loadcheck.sh.txt
index 4b5b0c9..6080b73 100644
--- a/fun/test/expected/macos/lib-loadcheck.sh.txt
+++ b/fun/test/expected/macos/lib-loadcheck.sh.txt
@@ -1,4 +1,5 @@
LOAD http-auth
+LOAD http-bearer
LOAD http-client
LOAD http-cors
LOAD http-server
@@ -66,4 +67,4 @@ LOAD lib-zlib
LOAD test
LOAD uuid
-loaded=61 failed=6
\ No newline at end of file
+loaded=62 failed=6
\ No newline at end of file
diff --git a/fun/test/lib-http-bearer-test.fun b/fun/test/lib-http-bearer-test.fun
new file mode 100644
index 0000000..cc646d1
--- /dev/null
+++ b/fun/test/lib-http-bearer-test.fun
@@ -0,0 +1,264 @@
+// fun/lib/http-bearer: self-driven checks. Raw requests go out over
+// ConnectSocket and the server is pumped with runOnce, so every assertion is
+// a stable boolean snapshot.
+use 'fun/lib/http-server.fun';
+use 'fun/lib/http-bearer.fun';
+
+fun hasRe(s, re)
+ result = false;
+ if s =~ re then result = true; end if;
+end fun;
+
+fun pump(s, n)
+ for i = 1 to n loop
+ exit when s.runOnce(300) = 0;
+ end loop;
+end fun;
+
+fun rq(port, raw)
+ var c = ConnectSocket('127.0.0.1', port);
+ SendAll(c, raw.escape());
+ result = c;
+end fun;
+
+fun getReq(port, path)
+ result = rq(port, 'GET ' & path & ' HTTP/1.1\r\nHost: t\r\n\r\n');
+end fun;
+
+fun verifyBearer(req, c)
+ if c['token'] = 'good-token' then
+ var p = new [];
+ p['sub'] = 'alice';
+ p['roles'] = new [];
+ p['roles']['admin'] = true;
+ result = p;
+ else
+ result = false;
+ end if;
+end fun;
+
+fun verifyTrue(req, c)
+ result = true;
+end fun;
+
+fun verifyBlocked(req, c)
+ if c['token'] = 'blocked' then
+ var r = new [];
+ r['status'] = 403;
+ r['message'] = 'blocked by policy';
+ var hs = new [];
+ hs['X-Reason'] = 'policy';
+ r['headers'] = hs;
+ result = r;
+ else
+ result = verifyBearer(req, c);
+ end if;
+end fun;
+
+fun verifyCustom(req, c)
+ var r = new [];
+ r['status'] = 401;
+ r['message'] = 'custom unauthorized';
+ var hs = new [];
+ hs['WWW-Authenticate'] = 'Bearer realm="custom"';
+ r['headers'] = hs;
+ result = r;
+end fun;
+
+fun verifyLeak(req, c)
+ var r = new [];
+ r['sub'] = 'leak';
+ r['token'] = 'secret';
+ result = r;
+end fun;
+
+fun bearerWho(req, res)
+ res.json([sub: req.auth['sub'], scheme: req.auth['scheme'], hasToken: req.auth['token'] <> nil, hasCred: req.credentials <> nil]);
+end fun;
+
+fun defaultWho(req, res)
+ res.json([scheme: req.auth['scheme']]);
+end fun;
+
+fun optionalWho(req, res)
+ if req.auth = nil then
+ res.text('anonymous');
+ else
+ res.text('sub:' & req.auth['sub']);
+ end if;
+end fun;
+
+fun rawWho(req, res)
+ if req.credentials = nil then
+ res.text('no-cred');
+ else
+ res.text('token:' & req.credentials['token']);
+ end if;
+end fun;
+
+var s = HttpServer(0);
+var api = s.group('/api');
+api.Use(HttpBearerAuth(verifyBearer, [realm: 'api']));
+api.get('/me', bearerWho);
+var opt = s.group('/opt');
+opt.Use(HttpBearerAuth(verifyBearer, [realm: 'opt', optional: true]));
+opt.get('/who', optionalWho);
+var raw = s.group('/raw');
+raw.Use(HttpBearerAuth());
+raw.get('/who', rawWho);
+var dflt = s.group('/default');
+dflt.Use(HttpBearerAuth(verifyTrue, [realm: 'def']));
+dflt.get('/who', defaultWho);
+var blocked = s.group('/blocked');
+blocked.Use(HttpBearerAuth(verifyBlocked, [realm: 'blocked']));
+blocked.get('/me', bearerWho);
+var cust = s.group('/custom');
+cust.Use(HttpBearerAuth(verifyCustom, [realm: 'ignored']));
+cust.get('/me', bearerWho);
+var leak = s.group('/leak');
+leak.Use(HttpBearerAuth(verifyLeak, [realm: 'leak']));
+leak.get('/me', bearerWho);
+var p = s.listen();
+
+var c1 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer good-token\r\n\r\n');
+pump(s, 5);
+var r1 = RecvSome(c1, 4096);
+?. 'bearer ok 200: ' & hasRe(r1, %^HTTP/1\.1\x20200%);
+?. 'bearer sub: ' & hasRe(r1, %"sub":"alice"%);
+?. 'bearer scheme: ' & hasRe(r1, %"scheme":"bearer"%);
+?. 'bearer no token in auth: ' & hasRe(r1, %"hasToken":false%);
+?. 'bearer credentials cleared: ' & hasRe(r1, %"hasCred":false%);
+CloseSocket(c1);
+
+var c2 = getReq(p, '/api/me');
+pump(s, 5);
+var r2 = RecvSome(c2, 4096);
+?. 'bearer missing 401: ' & hasRe(r2, %^HTTP/1\.1\x20401%);
+?. 'bearer missing challenge: ' & hasRe(r2, %WWW-Authenticate:\x20Bearer\x20realm="api"$%m);
+?. 'bearer missing no error: ' & not hasRe(r2, %error=%);
+CloseSocket(c2);
+
+var c3 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer nope\r\n\r\n');
+pump(s, 5);
+var r3 = RecvSome(c3, 4096);
+?. 'bearer wrong 401: ' & hasRe(r3, %^HTTP/1\.1\x20401%);
+?. 'bearer invalid token error: ' & hasRe(r3, %WWW-Authenticate:\x20Bearer\x20realm="api",\x20error="invalid_token"%);
+CloseSocket(c3);
+
+var c4 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer\r\n\r\n');
+pump(s, 5);
+var r4 = RecvSome(c4, 4096);
+?. 'bearer malformed 401: ' & hasRe(r4, %^HTTP/1\.1\x20401%);
+?. 'bearer invalid request error: ' & hasRe(r4, %WWW-Authenticate:\x20Bearer\x20realm="api",\x20error="invalid_request"%);
+CloseSocket(c4);
+
+var c4b = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer abc=def\r\n\r\n');
+pump(s, 5);
+var r4b = RecvSome(c4b, 4096);
+?. 'bearer bad padding 401: ' & hasRe(r4b, %^HTTP/1\.1\x20401%);
+CloseSocket(c4b);
+
+var c4c = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer =\r\n\r\n');
+pump(s, 5);
+var r4c = RecvSome(c4c, 4096);
+?. 'bearer only padding 401: ' & hasRe(r4c, %^HTTP/1\.1\x20401%);
+CloseSocket(c4c);
+
+var c5 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: bearer good-token\r\n\r\n');
+pump(s, 5);
+var r5 = RecvSome(c5, 4096);
+?. 'bearer lower scheme 200: ' & hasRe(r5, %^HTTP/1\.1\x20200%);
+CloseSocket(c5);
+
+var c5b = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer good-token\r\n\r\n');
+pump(s, 5);
+var r5b = RecvSome(c5b, 4096);
+?. 'bearer multiple spaces 200: ' & hasRe(r5b, %^HTTP/1\.1\x20200%);
+CloseSocket(c5b);
+
+var c6 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic YWxpY2U6c2VjcmV0\r\n\r\n');
+pump(s, 5);
+var r6 = RecvSome(c6, 4096);
+?. 'bearer rejects basic: ' & hasRe(r6, %^HTTP/1\.1\x20401%);
+CloseSocket(c6);
+
+var c7 = getReq(p, '/opt/who');
+pump(s, 5);
+var r7 = RecvSome(c7, 4096);
+?. 'bearer optional anonymous: ' & hasRe(r7, %\r\n\r\nanonymous$%);
+CloseSocket(c7);
+
+var c8 = rq(p, 'GET /opt/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer good-token\r\n\r\n');
+pump(s, 5);
+var r8 = RecvSome(c8, 4096);
+?. 'bearer optional user: ' & hasRe(r8, %\r\n\r\nsub:alice$%);
+CloseSocket(c8);
+
+var c9 = rq(p, 'GET /opt/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer nope\r\n\r\n');
+pump(s, 5);
+var r9 = RecvSome(c9, 4096);
+?. 'bearer optional bad 401: ' & hasRe(r9, %^HTTP/1\.1\x20401%);
+CloseSocket(c9);
+
+var c9b = rq(p, 'GET /opt/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer\r\n\r\n');
+pump(s, 5);
+var r9b = RecvSome(c9b, 4096);
+?. 'bearer optional malformed 401: ' & hasRe(r9b, %^HTTP/1\.1\x20401%);
+CloseSocket(c9b);
+
+var c10 = getReq(p, '/raw/who');
+pump(s, 5);
+var r10 = RecvSome(c10, 4096);
+?. 'bearer parse-only empty: ' & hasRe(r10, %\r\n\r\nno-cred$%);
+CloseSocket(c10);
+
+var c10b = rq(p, 'GET /raw/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer\r\n\r\n');
+pump(s, 5);
+var r10b = RecvSome(c10b, 4096);
+?. 'bearer parse-only malformed: ' & hasRe(r10b, %\r\n\r\nno-cred$%);
+CloseSocket(c10b);
+
+var c11 = rq(p, 'GET /raw/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer abc-._~+/=\r\n\r\n');
+pump(s, 5);
+var r11 = RecvSome(c11, 4096);
+?. 'bearer token68: ' & hasRe(r11, %\r\n\r\ntoken:abc-._~\+/=$%);
+CloseSocket(c11);
+
+var c12 = rq(p, 'GET /api/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer bad token\r\n\r\n');
+pump(s, 5);
+var r12 = RecvSome(c12, 4096);
+?. 'bearer bad token68 401: ' & hasRe(r12, %^HTTP/1\.1\x20401%);
+CloseSocket(c12);
+
+var c13 = rq(p, 'GET /default/who HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer good-token\r\n\r\n');
+pump(s, 5);
+var r13 = RecvSome(c13, 4096);
+?. 'bearer default true 200: ' & hasRe(r13, %^HTTP/1\.1\x20200%);
+?. 'bearer default scheme: ' & hasRe(r13, %"scheme":"bearer"%);
+CloseSocket(c13);
+
+var c14 = rq(p, 'GET /blocked/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer blocked\r\n\r\n');
+pump(s, 5);
+var r14 = RecvSome(c14, 4096);
+?. 'bearer blocked 403: ' & hasRe(r14, %^HTTP/1\.1\x20403%);
+?. 'bearer blocked header: ' & hasRe(r14, %X-Reason:\x20policy%);
+?. 'bearer blocked body: ' & hasRe(r14, %blocked\x20by\x20policy%);
+?. 'bearer blocked no challenge: ' & not hasRe(r14, %WWW-Authenticate%);
+CloseSocket(c14);
+
+var c15 = rq(p, 'GET /custom/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer custom\r\n\r\n');
+pump(s, 5);
+var r15 = RecvSome(c15, 4096);
+?. 'bearer custom 401: ' & hasRe(r15, %^HTTP/1\.1\x20401%);
+?. 'bearer custom challenge: ' & hasRe(r15, %WWW-Authenticate:\x20Bearer\x20realm="custom"%);
+?. 'bearer custom body: ' & hasRe(r15, %custom\x20unauthorized%);
+CloseSocket(c15);
+
+var c16 = rq(p, 'GET /leak/me HTTP/1.1\r\nHost: t\r\nAuthorization: Bearer good-token\r\n\r\n');
+pump(s, 5);
+var r16 = RecvSome(c16, 4096);
+?. 'bearer strips verifier token: ' & (hasRe(r16, %"sub":"leak"%) and hasRe(r16, %"hasToken":false%));
+CloseSocket(c16);
+
+s.stop();
+?. 'ok';
diff --git a/fun/test/regression-list.txt b/fun/test/regression-list.txt
index bb76c12..51fc4b4 100644
--- a/fun/test/regression-list.txt
+++ b/fun/test/regression-list.txt
@@ -55,6 +55,7 @@ ptr-primitives.fun test/ptr-primitives.fun snap
lib-func-check.fun test/lib-func-check.fun snap
lib-hash-test.fun test/lib-hash-test.fun snap
lib-http-test.fun test/lib-http-test.fun snap
+lib-http-bearer-test.fun test/lib-http-bearer-test.fun snap
lib-http-cors-test.fun test/lib-http-cors-test.fun snap
lib-proc-test.fun test/lib-proc-test.fun snap
lib-unicode-test.fun test/lib-unicode-test.fun snap
diff --git a/fun/test/run-windows-regression.bat b/fun/test/run-windows-regression.bat
index adb2b08..6c5ff51 100644
--- a/fun/test/run-windows-regression.bat
+++ b/fun/test/run-windows-regression.bat
@@ -63,7 +63,7 @@ rem The four portable FFI cases (ffi-int64/float/float-cb/callbacks) are NOT
rem listed here: they only need getapi plus libffitest.dll, so they run
rem when both are there and are added below when they are not. ffi-scalar
rem stays: it calls libc/libm directly (getpid does not exist on Windows).
-set "HOSTDEP= builtin-dispatch.fun lib-host-test.fun lib-param-linux.fun lib-cmdline-linux.fun lib-loadcheck.sh libc-outbuf.fun ptr-primitives.fun raw-address-getapi.fun lib-tcc-test.fun lib-jit-test.fun lib-asm-lnx.fun lib-asm-a64.fun lib-asm-x64.fun fractal-asm-x64.fun lib-hash-test.fun lib-http-test.fun lib-http-cors-test.fun lib-proc-test.fun lib-unicode-test.fun run-benchmarks.sh ffi-scalar.fun lib-os-dispatch-prototype.fun lib-crypt-lnx.fun lib-e2e-lnx.fun lib-ajax-lnx-test.fun run-ajax-lnx.sh lib-winsock-lnx-test.fun lib-netif-test.fun lib-ado-lnx-test.fun lib-ado-schema-lnx-test.fun lib-xml-lnx-test.fun lib-jsonrpc-lnx-test.fun lib-orm-rpc-lnx-test.fun lib-os-lnx-test.fun lib-cmdline-lnx-test.fun lib-zlib-test.fun lib-orm-rows.fun lib-file-test.fun lib-dyns-test.fun "
+set "HOSTDEP= builtin-dispatch.fun lib-host-test.fun lib-param-linux.fun lib-cmdline-linux.fun lib-loadcheck.sh libc-outbuf.fun ptr-primitives.fun raw-address-getapi.fun lib-tcc-test.fun lib-jit-test.fun lib-asm-lnx.fun lib-asm-a64.fun lib-asm-x64.fun fractal-asm-x64.fun lib-hash-test.fun lib-http-test.fun lib-http-bearer-test.fun lib-http-cors-test.fun lib-proc-test.fun lib-unicode-test.fun run-benchmarks.sh ffi-scalar.fun lib-os-dispatch-prototype.fun lib-crypt-lnx.fun lib-e2e-lnx.fun lib-ajax-lnx-test.fun run-ajax-lnx.sh lib-winsock-lnx-test.fun lib-netif-test.fun lib-ado-lnx-test.fun lib-ado-schema-lnx-test.fun lib-xml-lnx-test.fun lib-jsonrpc-lnx-test.fun lib-orm-rpc-lnx-test.fun lib-os-lnx-test.fun lib-cmdline-lnx-test.fun lib-zlib-test.fun lib-orm-rows.fun lib-file-test.fun lib-dyns-test.fun "
rem Tests gated only by the regex engine's absence: probed below, skipped while
rem REGEXOK=0. The CI win64 build links PCRE dynamically (pcre3.dll next to