From 886ca41b39a2c01ac0526297a35b437f7c401d32 Mon Sep 17 00:00:00 2001 From: taadis Date: Thu, 22 Oct 2026 17:58:41 +0800 Subject: [PATCH] add HttpCors middleware, demo, and tests --- README.md | 13 +- README.zh.md | 13 +- fun/demos/http-cors-demo/README.md | 86 ++++++ fun/demos/http-cors-demo/package.fun | 15 + fun/demos/http-cors-demo/src/main.fun | 24 ++ fun/demos/http-cors-demo/web/index.html | 55 ++++ fun/lib/http-cors.fun | 229 +++++++++++++++ fun/lib/http-server.fun | 85 +++--- fun/test/expected/lib-http-cors-test.fun.txt | 42 +++ fun/test/expected/lib-http-test.fun.txt | 6 +- fun/test/expected/lib-loadcheck.sh.txt | 3 +- fun/test/expected/macos/lib-loadcheck.sh.txt | 3 +- fun/test/lib-http-cors-test.fun | 277 +++++++++++++++++++ fun/test/lib-http-test.fun | 2 +- fun/test/regression-list.txt | 1 + fun/test/run-windows-regression.bat | 2 +- 16 files changed, 800 insertions(+), 56 deletions(-) create mode 100644 fun/demos/http-cors-demo/README.md create mode 100644 fun/demos/http-cors-demo/package.fun create mode 100644 fun/demos/http-cors-demo/src/main.fun create mode 100644 fun/demos/http-cors-demo/web/index.html create mode 100644 fun/lib/http-cors.fun create mode 100644 fun/test/expected/lib-http-cors-test.fun.txt create mode 100644 fun/test/lib-http-cors-test.fun diff --git a/README.md b/README.md index e52dd0a..48d2d84 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); -- 64 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`; +- 65 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** — 64 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings. +- **Self-hosted standard library** — 65 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, 64 modules) +│ ├── lib/ # Standard library (pure Fun, 65 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 (64 modules): +The standard library in [`fun/lib`](fun/lib) is written in pure Fun (65 modules): -> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 64 modules written in Fun itself. +> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 65 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` +- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `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` @@ -531,6 +531,7 @@ The standard library in [`fun/lib`](fun/lib) is written in pure Fun (64 modules) - [`fun/demos`](fun/demos) — full applications and benchmarks: the `notepad--` editor, `odbc-search`, and performance/assembly/JIT benchmarks. - [`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. --- diff --git a/README.zh.md b/README.zh.md index 9512e67..d20434d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -39,7 +39,7 @@ 一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。** - 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系; -- 自带 64 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`; +- 自带 65 个用 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` 接口)。 -- **自托管的标准库** —— 64 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 +- **自托管的标准库** —— 65 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 --- @@ -365,7 +365,7 @@ fun/ │ ├── utils/ # 工具函数 │ └── prj/fun/ # 工程与构建脚本(funcmd.dpr,Delphi/FPC) ├── fun/ -│ ├── lib/ # 标准库(纯 Fun 编写,64 个模块) +│ ├── lib/ # 标准库(纯 Fun 编写,65 个模块) │ ├── demo/ # 语言语法演示 │ ├── demos/ # 完整应用与基准测试 │ └── key_*.ini # 关键字别名包(CN / FR / PUA) @@ -477,16 +477,16 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala ## 标准库 -标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(64 个模块): +标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(65 个模块): -> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——64 个模块全部用 Fun 自身写成。 +> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——65 个模块全部用 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` +- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`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` @@ -508,6 +508,7 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala - [`fun/demos`](fun/demos) —— 完整应用与基准测试:记事本 `notepad--`、`odbc-search`,以及性能 / 汇编 / JIT 基准。 - [`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、暴露响应头与浏览器页面示例。 --- diff --git a/fun/demos/http-cors-demo/README.md b/fun/demos/http-cors-demo/README.md new file mode 100644 index 0000000..b0ab1a7 --- /dev/null +++ b/fun/demos/http-cors-demo/README.md @@ -0,0 +1,86 @@ +# http-cors-demo + +A small API and browser page showing the CORS middleware: + +- preflight `OPTIONS` is answered before routing and authentication; +- `credentials: true` requires an exact origin and sends + `Access-Control-Allow-Credentials: true`; +- `X-Request-Id` is exposed so browser JavaScript can read it; +- `Vary: Origin` keeps shared caches correct. + +```text +package.fun # manifest +src/main.fun # API on :8080 +web/index.html # page served from :5173 +``` + +## 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-cors-demo + +# terminal 1: API on http://127.0.0.1:8080 +fun run +# or: ../../../src/prj/fun/funcmd run + +# terminal 2: page on http://127.0.0.1:5173 +python3 -m http.server 5173 --bind 127.0.0.1 --directory web +``` + +Open and click the buttons. + +The allowed origin is exactly `http://127.0.0.1:5173`; opening the page from +another origin is rejected by the browser (preflight returns 403, actual +requests have no `Access-Control-Allow-Origin`). + +## Curl + +The browser sends the preflight automatically. To see it directly: + +```text +curl -i -X OPTIONS http://127.0.0.1:8080/api/public \ + -H 'Origin: http://127.0.0.1:5173' \ + -H 'Access-Control-Request-Method: GET' +``` + +Expected: `204` with `Access-Control-Allow-Origin`, +`Access-Control-Allow-Methods`, `Access-Control-Allow-Credentials`, +`Access-Control-Max-Age`, and `Vary`. + +A simple request: + +```text +curl -i http://127.0.0.1:8080/api/public \ + -H 'Origin: http://127.0.0.1:5173' +``` + +Expected: `200` with `Access-Control-Allow-Origin`, +`Access-Control-Allow-Credentials`, `Access-Control-Expose-Headers`, and +`Vary: Origin`. + +A JSON POST triggers a preflight for `Content-Type`: + +```text +curl -i -X OPTIONS http://127.0.0.1:8080/api/echo \ + -H 'Origin: http://127.0.0.1:5173' \ + -H 'Access-Control-Request-Method: POST' \ + -H 'Access-Control-Request-Headers: content-type' +``` + +## Notes + +- CORS is a browser read policy, not authentication or CSRF protection. The + demo endpoints are public. +- `origins: ['*']` is the default for quick public APIs. It cannot be combined + with `credentials: true`; credentialed APIs must list exact origins. +- Register CORS before authentication middleware. Preflight requests do not + carry credentials and must not be rejected by auth. +- `headers` omitted means "reflect the requested headers"; list them explicitly + when you want a fixed allowlist. +- In production, set explicit origins, use HTTPS, and keep `Vary: Origin` + intact through proxies/CDNs. diff --git a/fun/demos/http-cors-demo/package.fun b/fun/demos/http-cors-demo/package.fun new file mode 100644 index 0000000..879f183 --- /dev/null +++ b/fun/demos/http-cors-demo/package.fun @@ -0,0 +1,15 @@ +fun package() + result = [ + name: 'http-cors-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-cors-demo/src/main.fun b/fun/demos/http-cors-demo/src/main.fun new file mode 100644 index 0000000..1327b38 --- /dev/null +++ b/fun/demos/http-cors-demo/src/main.fun @@ -0,0 +1,24 @@ +use '../../../lib/http-server.fun'; +use '../../../lib/http-cors.fun'; + +fun publicH(req, res) + res.header('X-Request-Id', 'demo-1').json([ok: true, from: 'api']); +end fun; + +fun echoH(req, res) + res.json([got: req.json()]); +end fun; + +var s = HttpServer(8080); +s.Use(HttpLogger()); +s.Use(HttpCors([ + origins: ['http://127.0.0.1:5173'], + methods: ['GET', 'POST'], + headers: ['Content-Type', 'Authorization'], + exposeHeaders: ['X-Request-Id'], + credentials: true, + maxAge: 600 +])); +s.get('/api/public', publicH); +s.post('/api/echo', echoH); +s.run(); diff --git a/fun/demos/http-cors-demo/web/index.html b/fun/demos/http-cors-demo/web/index.html new file mode 100644 index 0000000..30cf079 --- /dev/null +++ b/fun/demos/http-cors-demo/web/index.html @@ -0,0 +1,55 @@ + + + + + + Fun CORS demo + + + +

Fun CORS demo

+

+ Page origin: http://127.0.0.1:5173
+ API origin: http://127.0.0.1:8080 +

+ + +
Click a button.
+ + + + diff --git a/fun/lib/http-cors.fun b/fun/lib/http-cors.fun new file mode 100644 index 0000000..212780a --- /dev/null +++ b/fun/lib/http-cors.fun @@ -0,0 +1,229 @@ +// Copyright (c) 2010-2026 Zhang Weidong +// SPDX-License-Identifier: MIT +// +// http-cors: CORS middleware following the Fetch Living Standard. +// +// s.Use(HttpCors([ +// origins: ['https://app.example.com', 'http://localhost:5173'], +// methods: ['GET', 'POST', 'PUT', 'DELETE'], +// headers: ['Content-Type', 'Authorization'], // nil = reflect request +// exposeHeaders: ['X-Request-Id'], +// credentials: true, +// maxAge: 600 +// ])); +// +// origins defaults to ['*']; credentials: true requires explicit origins and +// never uses the '*' response value. originCheck(origin) can replace origins +// for dynamic policies. Register it before authentication: preflight (OPTIONS +// with Access-Control-Request-Method) is answered here. CORS controls browser +// reads only; CSRF protection is a separate concern. +use 'lib-regex.fun'; + +fun _corsTrim(s) + var b = 0; + var e = s.length(); + while b < e and (s.substr(b, 1) = ' ' or s.substr(b, 1) = 9.toChar()) do b += 1; end do; + while e > b and (s.substr(e - 1, 1) = ' ' or s.substr(e - 1, 1) = 9.toChar()) do e -= 1; end do; + result = s.substr(b, e - b); +end fun; + +// comma-separated string or list -> list, trimmed, empty entries dropped +fun _corsList(v) + var out = new []; + if v = nil then result = out; return; end if; + if v.type() = 8 then + var s = _corsTrim('' & v); + if s <> '' then + var ps = split(/,/, s); + for i = 0 to ps.@count() - 1 do + var x = _corsTrim(ps[i]); + if x <> '' then out.@add(x); end if; + end do; + end if; + else + for i = 0 to v.@count() - 1 do + var x = _corsTrim('' & v[i]); + if x <> '' then out.@add(x); end if; + end do; + end if; + result = out; +end fun; + +fun _corsJoin(xs) + var s = ''; + for i = 0 to xs.@count() - 1 do + if s <> '' then s &= ', '; end if; + s &= xs[i]; + end do; + result = s; +end fun; + +class CCors(o) + var allowAny = false; + var origins = new []; + var originCheck = nil; + var methods = new []; + var methodsLine = ''; + var reflectHeaders = true; + var headers = new []; + var headersLine = ''; + var exposeLine = ''; + var credentials = false; + var maxAge = 600; + var optionsStatus = 204; + + if o <> nil then + if o.@has('credentials') and o['credentials'] = true then credentials = true; end if; + if o.@has('originCheck') then originCheck = o['originCheck']; end if; + if o.@has('maxAge') then + maxAge = o['maxAge']; + if maxAge < 0 then raise 'http-cors: maxAge must be >= 0'; end if; + end if; + if o.@has('optionsStatus') then + optionsStatus = o['optionsStatus']; + if optionsStatus <> 200 and optionsStatus <> 204 then + raise 'http-cors: optionsStatus must be 200 or 204'; + end if; + end if; + end if; + + var os = nil; + if o <> nil and o.@has('origins') then + os = o['origins']; + elsif originCheck <> nil then + os = new []; + else + os = '*'; + end if; + if os = nil then + if originCheck <> nil then os = new []; else os = '*'; end if; + end if; + var ol = _corsList(os); + if os.type() = 8 and _corsTrim('' & os) = '*' then + allowAny = true; + else + for i = 0 to ol.@count() - 1 do + var x = ol[i]; + if x = '*' then allowAny = true; else origins[x] = true; end if; + end do; + end if; + if allowAny and credentials then + raise 'http-cors: credentials requires explicit origins'; + end if; + + var ms = nil; + if o <> nil and o.@has('methods') then ms = o['methods']; else ms = 'GET,HEAD,POST,PUT,PATCH,DELETE'; end if; + var ml = _corsList(ms); + var mseen = new []; + for i = 0 to ml.@count() - 1 do + var m = ml[i].upper(); + if m <> '' and mseen[m] = nil then + mseen[m] = true; + methods[m] = true; + if methodsLine <> '' then methodsLine &= ', '; end if; + methodsLine &= m; + end if; + end do; + + if o <> nil and o.@has('headers') and o['headers'] <> nil then + reflectHeaders = false; + var hl = _corsList(o['headers']); + var hseen = new []; + for i = 0 to hl.@count() - 1 do + var h = hl[i]; + var hk = h.lower(); + if hk <> '' and hseen[hk] = nil then + hseen[hk] = true; + headers[hk] = true; + if headersLine <> '' then headersLine &= ', '; end if; + headersLine &= h; + end if; + end do; + end if; + + if o <> nil and o.@has('exposeHeaders') then + var el = _corsList(o['exposeHeaders']); + exposeLine = _corsJoin(el); + end if; + + fun originOk(origin) + if allowAny then result = true; return; end if; + if origins[origin] <> nil then result = true; return; end if; + if originCheck <> nil then result = originCheck(origin); return; end if; + result = false; + end fun; + + fun headersOk(ph) + if reflectHeaders then result = true; return; end if; + result = true; + if ph = '' then return; end if; + var hs = split(/,/, ph); + for i = 0 to hs.@count() - 1 do + var h = _corsTrim(hs[i]).lower(); + if h <> '' and headers[h] = nil then result = false; return; end if; + end do; + end fun; + + fun apply(res, origin) + var acao = '*'; + if credentials or not allowAny then acao = origin; end if; + res.header('Access-Control-Allow-Origin', acao); + if credentials then res.header('Access-Control-Allow-Credentials', 'true'); end if; + if exposeLine <> '' then res.header('Access-Control-Expose-Headers', exposeLine); end if; + if credentials or not allowAny then res.addHeader('Vary', 'Origin'); end if; + end fun; + + fun preflight(res, origin, ph) + res.status(optionsStatus); + var acao = '*'; + if credentials or not allowAny then acao = origin; end if; + res.header('Access-Control-Allow-Origin', acao); + if credentials then res.header('Access-Control-Allow-Credentials', 'true'); end if; + if methodsLine <> '' then res.header('Access-Control-Allow-Methods', methodsLine); end if; + if reflectHeaders then + if ph <> '' then res.header('Access-Control-Allow-Headers', ph); end if; + elsif headersLine <> '' then + res.header('Access-Control-Allow-Headers', headersLine); + end if; + if maxAge > 0 then res.header('Access-Control-Max-Age', '' & maxAge); end if; + res.addHeader('Vary', 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers'); + res.send(''); + end fun; + + fun deny(res, why) + res.header('Vary', 'Origin'); + res.status(403).text('cors: ' & why); + end fun; + + fun run(req, res, nx) + var origin = _corsTrim(req.header('origin') or ''); + if origin = '' then + if allowAny and not credentials then + apply(res, ''); + else + res.addHeader('Vary', 'Origin'); + end if; + nx(); + return; + end if; + var pm = req.header('access-control-request-method') or ''; + if req.method = 'OPTIONS' and pm <> '' then + if not originOk(origin) then deny(res, 'origin not allowed'); return; end if; + if methods[pm.upper()] = nil then deny(res, 'method not allowed'); return; end if; + var ph = req.header('access-control-request-headers') or ''; + if not headersOk(ph) then deny(res, 'header not allowed'); return; end if; + preflight(res, origin, ph); + else + if originOk(origin) then + apply(res, origin); + else + res.addHeader('Vary', 'Origin'); + end if; + nx(); + end if; + end fun; +end class; + +fun HttpCors(opts) + result = CCors(opts); +end fun; diff --git a/fun/lib/http-server.fun b/fun/lib/http-server.fun index 1116c88..cecea49 100644 --- a/fun/lib/http-server.fun +++ b/fun/lib/http-server.fun @@ -43,6 +43,9 @@ // Notes: // * middleware is a function (req, res, nx) or a stateful object with a // run(req, res, nx) method; Use accepts either form. +// * HttpServer.Use runs before route matching (all requests, including +// 404/405); HttpGroup.Use runs after a route matches. Put request-wide +// middleware such as CORS on the server, before authentication. // * 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 @@ -337,6 +340,50 @@ fun _runChain(mws, i, req, res, h) end if; end fun; +// route dispatch after the global middleware chain +fun _dispatchFinal(req, res) + _dispatch(_HTTP_APP, req, res); +end fun; + +fun _dispatch(app, req, res) + var segs = _segments(req.path); + var best = nil; + var bestSc = -1; + var allow = new []; + for rt in app.routes do + var ps = _matchPath(segs, rt.segs); + if ps <> nil then + if rt.method = req.method then + var sc = _scorePath(rt.segs); + if sc > bestSc then + best = rt; + bestSc = sc; + req.params = ps; + end if; + else + allow.@add(rt.method); + end if; + end if; + end do; + + if best = nil then + if allow.@count() > 0 then + var av = ''; + for m in allow do + if av <> '' then av &= ', '; end if; + av &= m; + end do; + res.status(405).header('Allow', av).text('method not allowed'); + else + res.status(404).text('not found'); + end if; + return; + end if; + + req.route = best; + _runChain(best.mws, 0, req, res, best.handler); +end fun; + //-------------------------------------------------------------- // request / response //-------------------------------------------------------------- @@ -676,49 +723,13 @@ class HttpServer(addr) r.path = req; end if; - // pick the most specific route under this method - var segs = _segments(r.path); - var best = nil; - var bestSc = -1; - var allow = new []; - for rt in app.routes do - var ps = _matchPath(segs, rt.segs); - if ps <> nil then - if rt.method = r.method then - var sc = _scorePath(rt.segs); - if sc > bestSc then - best = rt; - bestSc = sc; - r.params = ps; - end if; - else - allow.@add(rt.method); - end if; - end if; - end do; - var res = HttpRes(); res.sock = sock; res.server = app.http; res.hdrs = new []; - if best = nil then - if allow.@count() > 0 then - var av = ''; - for m in allow do - if av <> '' then av &= ', '; end if; - av &= m; - end do; - res.status(405).header('Allow', av).text('method not allowed'); - else - res.status(404).text('not found'); - end if; - return; - end if; - - r.route = best; try - _runChain(_joinList(app.mws, best.mws), 0, r, res, best.handler); + _runChain(app.mws, 0, r, res, _dispatchFinal); except if not res.sent then res.status(500).header('Content-Type', 'text/plain; charset=utf-8').send('Internal Error: ' & @); diff --git a/fun/test/expected/lib-http-cors-test.fun.txt b/fun/test/expected/lib-http-cors-test.fun.txt new file mode 100644 index 0000000..8b0c2db --- /dev/null +++ b/fun/test/expected/lib-http-cors-test.fun.txt @@ -0,0 +1,42 @@ +cors preflight 204: True +cors allow origin: True +cors allow methods: True +cors allow headers: True +cors max age: True +cors preflight vary: True +cors lowercase preflight: True +cors bad origin 403: True +cors bad origin no acao: True +cors bad method 403: True +cors bad header 403: True +cors actual 200: True +cors actual acao: True +cors actual expose: True +cors actual vary: True +cors second origin: True +cors actual bad origin no acao: True +cors explicit null origin: True +cors vary merge: True +cors no origin no acao: True +cors 404 keeps acao: True +cors 500 keeps acao: True +cors 405 keeps acao: True +cors preflight bypass auth: True +cors preflight not 401: True +cors 401 keeps acao: True +cors preflight unknown path: True +cors credentials acao: True +cors credentials acac: True +cors credentials preflight: True +cors default wildcard: True +cors default no credentials: True +cors default preflight: True +cors default reflect header: True +cors origincheck allow: True +cors origincheck deny: True +cors origincheck preflight: True +cors string origin: True +cors options status 200: True +cors max age zero: True +cors wildcard credentials rejected: True +ok diff --git a/fun/test/expected/lib-http-test.fun.txt b/fun/test/expected/lib-http-test.fun.txt index cec59f8..7ebb2cb 100644 --- a/fun/test/expected/lib-http-test.fun.txt +++ b/fun/test/expected/lib-http-test.fun.txt @@ -10,12 +10,12 @@ head 405: True head allow: True post echo: True del 405: True -order untouched: A>a>a>a>a>a>a>