From d95a52f795a2ad9a004c621c6df7f9ca3cec2188 Mon Sep 17 00:00:00 2001 From: taadis Date: Thu, 22 Oct 2026 15:30:00 +0800 Subject: [PATCH] add HttpBasicAuth middleware with verifier hook --- README.md | 12 +- README.zh.md | 12 +- fun/lib/http-auth.fun | 171 ++++++++++++++++ fun/lib/http-server.fun | 13 +- fun/test/expected/lib-http-test.fun.txt | 24 +++ fun/test/expected/lib-loadcheck.sh.txt | 3 +- fun/test/expected/macos/lib-loadcheck.sh.txt | 3 +- fun/test/lib-http-test.fun | 203 ++++++++++++++++++- 8 files changed, 424 insertions(+), 17 deletions(-) create mode 100644 fun/lib/http-auth.fun diff --git a/README.md b/README.md index e47f5fe..e52dd0a 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); -- 63 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`; +- 64 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** — 63 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings. +- **Self-hosted standard library** — 64 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, 63 modules) +│ ├── lib/ # Standard library (pure Fun, 64 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 (63 modules): +The standard library in [`fun/lib`](fun/lib) is written in pure Fun (64 modules): -> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 63 modules written in Fun itself. +> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 64 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` +- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth` - **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` diff --git a/README.zh.md b/README.zh.md index 3aa04d9..9512e67 100644 --- a/README.zh.md +++ b/README.zh.md @@ -39,7 +39,7 @@ 一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。** - 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系; -- 自带 63 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`; +- 自带 64 个用 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` 接口)。 -- **自托管的标准库** —— 63 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 +- **自托管的标准库** —— 64 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 --- @@ -365,7 +365,7 @@ fun/ │ ├── utils/ # 工具函数 │ └── prj/fun/ # 工程与构建脚本(funcmd.dpr,Delphi/FPC) ├── fun/ -│ ├── lib/ # 标准库(纯 Fun 编写,63 个模块) +│ ├── lib/ # 标准库(纯 Fun 编写,64 个模块) │ ├── demo/ # 语言语法演示 │ ├── demos/ # 完整应用与基准测试 │ └── key_*.ini # 关键字别名包(CN / FR / PUA) @@ -477,16 +477,16 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala ## 标准库 -标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(63 个模块): +标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(64 个模块): -> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——63 个模块全部用 Fun 自身写成。 +> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——64 个模块全部用 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` +- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth` - **数据库**:`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` diff --git a/fun/lib/http-auth.fun b/fun/lib/http-auth.fun new file mode 100644 index 0000000..deb3fdb --- /dev/null +++ b/fun/lib/http-auth.fun @@ -0,0 +1,171 @@ +// Copyright (c) 2010-2026 Zhang Weidong +// SPDX-License-Identifier: MIT +// +// http-auth: HTTP Basic authentication middleware (RFC 7617). Other schemes +// can reuse the credential and principal shapes. +// +// use 'http-auth.fun'; +// +// fun verifyBasic(req, c) +// var u = Users.find(c['user']); // DB / cache / external +// if u = nil then return false; end if +// if not checkPassword(u, c['pass']) then return false; end if +// result = [id: u.id, user: u.name, roles: u.roles]; +// end fun; +// +// app.Use(HttpBasicAuth(verifyBasic, [realm: 'api'])); +// +// Options: realm (default 'Restricted'), optional (true allows a missing +// Authorization header; invalid credentials still fail). +// +// `verify(req, credentials)` is the application hook: it receives the request +// and the parsed credentials, and returns: +// * false / nil -> 401 (default challenge) +// * true -> success, default principal +// * 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. Basic sends credentials as base64, not encryption, +// so use HTTPS. Authentication only: role/policy checks stay in the handler. +// +// credentials is scheme-specific: Basic is +// [scheme: 'basic', user: '...', pass: '...']. +// In verifier mode the password is cleared after verification; req.auth holds +// the principal only. With no verifier, HttpBasicAuth() is parse-only: it sets +// req.credentials and calls nx(), and the application owns the policy. +use 'lib-base64.fun'; + +fun _authWs(c) + result = c = ' ' or c = 9.toChar(); +end fun; + +fun _authTrim(s) + var b = 0; + var e = s.length(); + while b < e and _authWs(s.substr(b, 1)) do b += 1; end do; + while e > b and _authWs(s.substr(e - 1, 1)) do e -= 1; end do; + result = s.substr(b, e - b); +end fun; + +fun _authB64Ok(s) + result = true; + var i = 0; + while i < s.length() do + if 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='.subpos(s.substr(i, 1)) < 0 then + result = false; + return; + end if; + i += 1; + end do; +end fun; + +// Basic payload -> credentials set, or nil when malformed +fun _authBasic(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() <> 'basic' then return; end if; + var b = _authTrim(h.substr(sp + 1)); + if b = '' or not _authB64Ok(b) then return; end if; + var raw = fromBase64(b); + var c = raw.subpos(':'); + if c <= 0 then return; end if; + var u = raw.substr(0, c); + var p = raw.substr(c + 1); + var r = new []; + r['scheme'] = 'basic'; + r['user'] = u; + r['pass'] = p; + result = r; +end fun; + +fun _authRealm(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(), ''); // backslash would escape the closing quote + result = s; + end if; + end if; +end fun; + +fun _authFail(res, code, message, headers, realm) + 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 + res.header('WWW-Authenticate', 'Basic realm="' & realm & '"'); + end if; + if message = nil or message = '' then message = 'unauthorized'; end if; + res.send(message); +end fun; + +class CBasicAuth(v, r, o) + var verify = v; + var realm = r; + var optional = o; + + fun run(req, res, nx) + var h = req.header('authorization'); + var cred = _authBasic(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(); + else + _authFail(res, 401, 'unauthorized', nil, realm); + end if; + else + req.credentials = cred; + var r = verify(req, cred); + req.credentials = nil; + if r = false or r = nil then + _authFail(res, 401, 'unauthorized', nil, realm); + elsif r = true then + var p = new []; + p['scheme'] = 'basic'; + p['user'] = cred['user']; + req.auth = p; + nx(); + else + var st = r['status']; + if st <> nil and st >= 400 then + _authFail(res, st, r['message'], r['headers'], realm); + else + var p = new []; + for k: v in r do p[k] = v; end do; + if p['scheme'] = nil then p['scheme'] = 'basic'; end if; + if p['user'] = nil then p['user'] = cred['user']; end if; + req.auth = p; + nx(); + end if; + end if; + end if; + end fun; +end class; + +fun HttpBasicAuth(verify, opts) + var realm = _authRealm(opts); + var optional = false; + if opts <> nil and opts['optional'] = true then optional = true; end if; + result = CBasicAuth(verify, realm, optional); +end fun; diff --git a/fun/lib/http-server.fun b/fun/lib/http-server.fun index 9f2fc54..1116c88 100644 --- a/fun/lib/http-server.fun +++ b/fun/lib/http-server.fun @@ -41,6 +41,8 @@ // // or: var port = s.listen(); ... s.runOnce(200) to drive yourself // // Notes: +// * middleware is a function (req, res, nx) or a stateful object with a +// run(req, res, nx) method; Use accepts either form. // * the middleware continuation parameter is named `nx` because `next` // (like `step`) is reserved by the language. // * routing rules: literal segments beat `:name` parameters, which beat the @@ -319,12 +321,19 @@ fun _scorePath(pat) result = sc; end fun; -// run the middleware chain, then the handler; nx is a zero-argument thunk +// run the middleware chain, then the handler; nx is a zero-argument thunk. +// A middleware is a function (req, res, nx) or an object with a run method +// (stateful middleware such as auth options). fun _runChain(mws, i, req, res, h) if mws = nil or i >= mws.@count() then h(req, res); else - mws[i](req, res, -> _runChain(mws, i + 1, req, res, h)); + var m = mws[i]; + if m.run <> nil then + m.run(req, res, -> _runChain(mws, i + 1, req, res, h)); + else + m(req, res, -> _runChain(mws, i + 1, req, res, h)); + end if; end if; end fun; diff --git a/fun/test/expected/lib-http-test.fun.txt b/fun/test/expected/lib-http-test.fun.txt index 025caf6..cec59f8 100644 --- a/fun/test/expected/lib-http-test.fun.txt +++ b/fun/test/expected/lib-http-test.fun.txt @@ -32,6 +32,30 @@ cookie cleared: True multi header: True bad header 500: True bad header message: True +auth ok 200: True +auth user: True +auth role: True +auth missing 401: True +auth challenge: True +auth wrong 401: True +auth blocked 403: True +auth blocked header: True +auth blocked body: True +auth optional anonymous: True +auth optional user: True +auth optional bad 401: True +auth parse-only empty: True +auth parse-only user: True +auth malformed 401: True +auth lower scheme 200: True +auth colon pass 200: True +auth colon user: True +auth default true 200: True +auth default user: True +auth default scheme: True +auth custom 401: True +auth custom challenge: True +auth custom body: True addr ip:port: True addr :port: True ok diff --git a/fun/test/expected/lib-loadcheck.sh.txt b/fun/test/expected/lib-loadcheck.sh.txt index 28ea513..ea5dad2 100644 --- a/fun/test/expected/lib-loadcheck.sh.txt +++ b/fun/test/expected/lib-loadcheck.sh.txt @@ -1,3 +1,4 @@ +LOAD http-auth LOAD http-client LOAD http-server LOAD lib-ado-schema @@ -64,4 +65,4 @@ LOAD lib-zlib LOAD test LOAD uuid -loaded=59 failed=6 \ No newline at end of file +loaded=60 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 31c25c0..2386d9d 100644 --- a/fun/test/expected/macos/lib-loadcheck.sh.txt +++ b/fun/test/expected/macos/lib-loadcheck.sh.txt @@ -1,3 +1,4 @@ +LOAD http-auth LOAD http-client LOAD http-server LOAD lib-ado-schema @@ -64,4 +65,4 @@ LOAD lib-zlib LOAD test LOAD uuid -loaded=59 failed=6 \ No newline at end of file +loaded=60 failed=6 \ No newline at end of file diff --git a/fun/test/lib-http-test.fun b/fun/test/lib-http-test.fun index 133511b..da9a15b 100644 --- a/fun/test/lib-http-test.fun +++ b/fun/test/lib-http-test.fun @@ -3,6 +3,8 @@ // every assertion prints a boolean -- a stable snapshot with no external // client. use 'fun/lib/http-server.fun'; +use 'fun/lib/http-auth.fun'; +use 'fun/lib/lib-base64.fun'; fun hasRe(s, re) result = false; @@ -95,6 +97,85 @@ fun badHeaderH(req, res) res.text('bad'); end fun; +var users = new []; +var ua = new []; +ua['pass'] = 'secret'; +ua['name'] = 'alice'; +ua['roles'] = new []; +ua['roles']['admin'] = true; +users['alice'] = ua; + +var ub = new []; +ub['pass'] = 'p:a'; +ub['name'] = 'bob'; +ub['roles'] = new []; +users['bob'] = ub; + +fun verifyBasic(req, c) + var u = users[c['user']]; + if u = nil or u['pass'] <> c['pass'] then + result = false; + return; + end if; + var p = new []; + p['ok'] = true; + p['user'] = u['name']; + p['roles'] = u['roles']; + result = p; +end fun; + +fun verifyBlocked(req, c) + if c['user'] = '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 = verifyBasic(req, c); + end if; +end fun; + +fun verifyTrue(req, c) + result = true; +end fun; + +fun verifyCustom(req, c) + var r = new []; + r['status'] = 401; + r['message'] = 'custom unauthorized'; + var hs = new []; + hs['WWW-Authenticate'] = 'Basic realm="custom"'; + r['headers'] = hs; + result = r; +end fun; + +fun defaultWho(req, res) + res.json([user: req.auth['user'], scheme: req.auth['scheme']]); +end fun; + +fun secureMe(req, res) + res.json([user: req.auth['user'], admin: req.auth['roles']['admin']]); +end fun; + +fun optionalWho(req, res) + if req.auth = nil then + res.text('anonymous'); + else + res.text('user:' & req.auth['user']); + end if; +end fun; + +fun rawWho(req, res) + if req.credentials = nil then + res.text('no-cred'); + else + res.text('user:' & req.credentials['user']); + end if; +end fun; + fun boom(req, res) raise 'kaboom'; end fun; @@ -115,6 +196,24 @@ s.get('/clearcookie', clearCookieH); s.get('/multi', multiH); s.get('/badheader', badHeaderH); s.get('/boom', boom); +var secure = s.group('/secure'); +secure.Use(HttpBasicAuth(verifyBasic, [realm: 'api'])); +secure.get('/me', secureMe); +var opt = s.group('/opt'); +opt.Use(HttpBasicAuth(verifyBasic, [realm: 'opt', optional: true])); +opt.get('/who', optionalWho); +var raw = s.group('/raw'); +raw.Use(HttpBasicAuth()); +raw.get('/who', rawWho); +var blocked = s.group('/blocked'); +blocked.Use(HttpBasicAuth(verifyBlocked, [realm: 'blocked'])); +blocked.get('/me', secureMe); +var dflt = s.group('/default'); +dflt.Use(HttpBasicAuth(verifyTrue, [realm: 'def'])); +dflt.get('/who', defaultWho); +var cust = s.group('/custom'); +cust.Use(HttpBasicAuth(verifyCustom, [realm: 'ignored'])); +cust.get('/me', hello); var api = s.group('/api'); api.Use(authMw); api.get('/orders', ordersH); @@ -285,7 +384,109 @@ var r20 = RecvSome(c20, 4096); CloseSocket(c20); //-------------------------------------------------------------- -// 10. address strings: 'ip:port' and ':port' (all interfaces) +// 10. Basic auth middleware +//-------------------------------------------------------------- +var goodB64 = Str2Base64('alice:secret'); +var c21 = rq(p, 'GET /secure/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & goodB64 & '\r\n\r\n'); +pump(s, 5); +var r21 = RecvSome(c21, 4096); +?. 'auth ok 200: ' & hasRe(r21, %^HTTP/1\.1\x20200%); +?. 'auth user: ' & hasRe(r21, %"user":"alice"%); +?. 'auth role: ' & hasRe(r21, %"admin":true%); +CloseSocket(c21); + +var c22 = getReq(p, '/secure/me'); +pump(s, 5); +var r22 = RecvSome(c22, 4096); +?. 'auth missing 401: ' & hasRe(r22, %^HTTP/1\.1\x20401%); +?. 'auth challenge: ' & hasRe(r22, %WWW-Authenticate:\x20Basic\x20realm="api"%); +CloseSocket(c22); + +var badB64 = Str2Base64('alice:wrong'); +var c23 = rq(p, 'GET /secure/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & badB64 & '\r\n\r\n'); +pump(s, 5); +var r23 = RecvSome(c23, 4096); +?. 'auth wrong 401: ' & hasRe(r23, %^HTTP/1\.1\x20401%); +CloseSocket(c23); + +var blockB64 = Str2Base64('blocked:x'); +var c24 = rq(p, 'GET /blocked/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & blockB64 & '\r\n\r\n'); +pump(s, 5); +var r24 = RecvSome(c24, 4096); +?. 'auth blocked 403: ' & hasRe(r24, %^HTTP/1\.1\x20403%); +?. 'auth blocked header: ' & hasRe(r24, %X-Reason:\x20policy%); +?. 'auth blocked body: ' & hasRe(r24, %blocked\x20by\x20policy%); +CloseSocket(c24); + +var c25 = getReq(p, '/opt/who'); +pump(s, 5); +var r25 = RecvSome(c25, 4096); +?. 'auth optional anonymous: ' & hasRe(r25, %\r\n\r\nanonymous$%); +CloseSocket(c25); + +var c26 = rq(p, 'GET /opt/who HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & goodB64 & '\r\n\r\n'); +pump(s, 5); +var r26 = RecvSome(c26, 4096); +?. 'auth optional user: ' & hasRe(r26, %\r\n\r\nuser:alice$%); +CloseSocket(c26); + +var c26b = rq(p, 'GET /opt/who HTTP/1.1\r\nHost: t\r\nAuthorization: Basic !!!\r\n\r\n'); +pump(s, 5); +var r26b = RecvSome(c26b, 4096); +?. 'auth optional bad 401: ' & hasRe(r26b, %^HTTP/1\.1\x20401%); +CloseSocket(c26b); + +var c27 = getReq(p, '/raw/who'); +pump(s, 5); +var r27 = RecvSome(c27, 4096); +?. 'auth parse-only empty: ' & hasRe(r27, %\r\n\r\nno-cred$%); +CloseSocket(c27); + +var c28 = rq(p, 'GET /raw/who HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & goodB64 & '\r\n\r\n'); +pump(s, 5); +var r28 = RecvSome(c28, 4096); +?. 'auth parse-only user: ' & hasRe(r28, %\r\n\r\nuser:alice$%); +CloseSocket(c28); + +var c29 = rq(p, 'GET /secure/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic !!!\r\n\r\n'); +pump(s, 5); +var r29 = RecvSome(c29, 4096); +?. 'auth malformed 401: ' & hasRe(r29, %^HTTP/1\.1\x20401%); +CloseSocket(c29); + +var c30 = rq(p, 'GET /secure/me HTTP/1.1\r\nHost: t\r\nAuthorization: basic ' & goodB64 & '\r\n\r\n'); +pump(s, 5); +var r30 = RecvSome(c30, 4096); +?. 'auth lower scheme 200: ' & hasRe(r30, %^HTTP/1\.1\x20200%); +CloseSocket(c30); + +var bobB64 = Str2Base64('bob:p:a'); +var c31 = rq(p, 'GET /secure/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & bobB64 & '\r\n\r\n'); +pump(s, 5); +var r31 = RecvSome(c31, 4096); +?. 'auth colon pass 200: ' & hasRe(r31, %^HTTP/1\.1\x20200%); +?. 'auth colon user: ' & hasRe(r31, %"user":"bob"%); +CloseSocket(c31); + +var c32 = rq(p, 'GET /default/who HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & goodB64 & '\r\n\r\n'); +pump(s, 5); +var r32 = RecvSome(c32, 4096); +?. 'auth default true 200: ' & hasRe(r32, %^HTTP/1\.1\x20200%); +?. 'auth default user: ' & hasRe(r32, %"user":"alice"%); +?. 'auth default scheme: ' & hasRe(r32, %"scheme":"basic"%); +CloseSocket(c32); + +var customB64 = Str2Base64('custom:x'); +var c33 = rq(p, 'GET /custom/me HTTP/1.1\r\nHost: t\r\nAuthorization: Basic ' & customB64 & '\r\n\r\n'); +pump(s, 5); +var r33 = RecvSome(c33, 4096); +?. 'auth custom 401: ' & hasRe(r33, %^HTTP/1\.1\x20401%); +?. 'auth custom challenge: ' & hasRe(r33, %WWW-Authenticate:\x20Basic\x20realm="custom"%); +?. 'auth custom body: ' & hasRe(r33, %custom\x20unauthorized%); +CloseSocket(c33); + +//-------------------------------------------------------------- +// 11. address strings: 'ip:port' and ':port' (all interfaces) //-------------------------------------------------------------- var s2 = HttpServer('127.0.0.1:0'); ?. 'addr ip:port: ' & (s2.listen() > 0);