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