Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
- 66 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`;
- 67 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.**
Expand Down Expand Up @@ -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** — 66 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.
- **Self-hosted standard library** — 67 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings.

---

Expand Down Expand Up @@ -372,7 +372,7 @@ fun/
│ ├── utils/ # Utility functions
│ └── prj/fun/ # Projects & build scripts (funcmd.dpr, Delphi/FPC)
├── fun/
│ ├── lib/ # Standard library (pure Fun, 66 modules)
│ ├── lib/ # Standard library (pure Fun, 67 modules)
│ ├── demo/ # Language syntax demos
│ ├── demos/ # Full applications & benchmarks
│ └── key_*.ini # Keyword alias packs (CN / FR / PUA)
Expand Down Expand Up @@ -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 (66 modules):
The standard library in [`fun/lib`](fun/lib) is written in pure Fun (67 modules):

> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 66 modules written in Fun itself.
> In total roughly 6,000 lines of logical code (excluding comments), about 100 lines per module — all 67 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-bearer`, `http-cors`
- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `http-bearer`, `http-cors`, `http-session`
- **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`
Expand All @@ -533,6 +533,7 @@ The standard library in [`fun/lib`](fun/lib) is written in pure Fun (66 modules)
- [`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.
- [`fun/demos/http-session-demo`](fun/demos/http-session-demo) — cookie session login/logout, SID rotation, and server-side state.

---

Expand Down
13 changes: 7 additions & 6 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。**

- 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系;
- 自带 66 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
- 自带 67 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
- 在信创与 AI Agent 场景中,作为连接国产 OS、数据库、芯片与业务逻辑的轻量级胶水。

Fun 的设计出发点很简单:**数据与代码都应该是头等公民,而且一段脚本应该能在不离开语言的情况下一路触达机器底层。**
Expand Down Expand Up @@ -70,7 +70,7 @@ Fun 使用 Pascal(Delphi / Free Pascal)编写。核心是一个小巧的树
- **关键字别名** —— 规范英文关键字 + 可插拔的别名包(中文、法文、自定义),运行时选择(见[下文](#关键字别名))。
- **跨平台** —— Windows(32/64 位)、Linux、ARM 与 Windows CE。
- **双形态分发** —— 命令行 `fun.exe` 与嵌入运行时 `fun.dll`(导出简单的 `Run` 接口)。
- **自托管的标准库** —— 66 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。
- **自托管的标准库** —— 67 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。

---

Expand Down Expand Up @@ -365,7 +365,7 @@ fun/
│ ├── utils/ # 工具函数
│ └── prj/fun/ # 工程与构建脚本(funcmd.dpr,Delphi/FPC)
├── fun/
│ ├── lib/ # 标准库(纯 Fun 编写,66 个模块)
│ ├── lib/ # 标准库(纯 Fun 编写,67 个模块)
│ ├── demo/ # 语言语法演示
│ ├── demos/ # 完整应用与基准测试
│ └── key_*.ini # 关键字别名包(CN / FR / PUA)
Expand Down Expand Up @@ -477,16 +477,16 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala

## 标准库

标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(66 个模块):
标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(67 个模块):

> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——66 个模块全部用 Fun 自身写成。
> 总代码量约 6,000 行逻辑代码(不含注释),平均每个模块约 100 行——67 个模块全部用 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-bearer`、`http-cors`
- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`http-bearer`、`http-cors`、`http-session`
- **数据库**:`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`
Expand All @@ -510,6 +510,7 @@ float/混合参数与 `@toCallback` 闭包在 win64 上也有覆盖;`ffi-scala
- [`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 挑战与同源页面示例。
- [`fun/demos/http-session-demo`](fun/demos/http-session-demo) —— cookie session 登录/登出、SID 轮换与服务端状态示例。

---

Expand Down
68 changes: 68 additions & 0 deletions fun/demos/http-session-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# http-session-demo

A small login/session page:

- `POST /api/login` stores the user in `req.session` and rotates the SID;
- `GET /api/me` reads the session;
- `GET /api/count` increments a value stored server-side;
- `POST /api/logout` sets `req.session = nil`, destroying the session and cookie.

```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-session-demo
fun run
# or: ../../../src/prj/fun/funcmd run
```

Open <http://127.0.0.1:8080/>. Login, then click `GET /api/me` and
`GET /api/count`; the browser sends the `sid` cookie automatically.

## Curl

Use a cookie jar:

```text
curl -i -c /tmp/fun-sid.txt -b /tmp/fun-sid.txt \
-X POST http://127.0.0.1:8080/api/login \
-H 'Content-Type: application/json' \
-d '{"user":"alice"}'
```

The response sets `sid=...; Path=/; HttpOnly; SameSite=Lax`. Then:

```text
curl -i -c /tmp/fun-sid.txt -b /tmp/fun-sid.txt http://127.0.0.1:8080/api/me
curl -i -c /tmp/fun-sid.txt -b /tmp/fun-sid.txt http://127.0.0.1:8080/api/count
curl -i -c /tmp/fun-sid.txt -b /tmp/fun-sid.txt -X POST http://127.0.0.1:8080/api/logout
```

After logout the cookie is cleared and `/api/me` returns `{"user":null}`.

## Notes

- Only a random SID is stored in the cookie; session data is in the server.
- `SessionRegenerate(req)` after login prevents session fixation; the old SID
is destroyed.
- Session data must be JSON-serializable (it is compared before/after the
handler to decide whether to save).
- The default cookie is `HttpOnly`, `SameSite=Lax`, `Path=/`; set `secure: true`
behind HTTPS.
- SameSite helps with CSRF but is not a complete CSRF defense. State-changing
routes should still use a CSRF token.
- The in-memory store is per process. For multiple instances, pass a shared
`store` implementing `get(sid)`, `set(sid, data, ttl)`, `destroy(sid)`.
- For a cross-site SPA, CORS must allow credentials with an exact origin, the
cookie needs `secure: true` and `sameSite: 'None'`, and browser third-party
cookie restrictions may still block it; a same-site/BFF deployment is safer.
15 changes: 15 additions & 0 deletions fun/demos/http-session-demo/package.fun
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fun package()
result = [
name: 'http-session-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;
42 changes: 42 additions & 0 deletions fun/demos/http-session-demo/src/main.fun
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use '../../../lib/http-server.fun';
use '../../../lib/http-session.fun';

fun meH(req, res)
if req.session['user'] = nil then
res.json([user: nil]);
else
res.json([user: req.session['user']]);
end if;
end fun;

fun loginH(req, res)
var body = req.json();
var user = body['user'];
if user = nil or user = '' then
res.status(400).json([error: 'user required']);
return;
end if;
req.session['user'] = user;
SessionRegenerate(req);
res.json([user: user]);
end fun;

fun countH(req, res)
req.session['count'] = (req.session['count'] or 0) + 1;
res.json([count: req.session['count']]);
end fun;

fun logoutH(req, res)
req.session = nil;
res.json([ok: true]);
end fun;

var s = HttpServer(8080);
s.Use(HttpLogger());
s.Use(HttpSession([name: 'sid', ttl: 86400, maxAge: 0, sameSite: 'Lax']));
s.get('/api/me', meH);
s.post('/api/login', loginH);
s.get('/api/count', countH);
s.post('/api/logout', logoutH);
s.static('/', 'web');
s.run();
52 changes: 52 additions & 0 deletions fun/demos/http-session-demo/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fun session demo</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 760px; padding: 0 1rem; }
input { padding: .4rem; width: 14rem; }
button { margin: 0 .5rem .5rem 0; padding: .4rem .8rem; }
pre { background: #f4f4f4; padding: 1rem; overflow: auto; white-space: pre-wrap; }
code { background: #eee; padding: .1rem .3rem; }
</style>
</head>
<body>
<h1>Fun session demo</h1>
<p>Cookie <code>sid</code> carries only a random session id; data stays on the server.</p>
<p><input id="user" value="alice"></p>
<button id="login">POST /api/login</button>
<button id="me">GET /api/me</button>
<button id="count">GET /api/count</button>
<button id="logout">POST /api/logout</button>
<pre id="out">Click a button.</pre>

<script>
const out = document.getElementById('out');

async function call(label, path, opts) {
out.textContent = label + ' ...';
try {
const r = await fetch(path, opts || {});
const body = await r.text();
out.textContent = label + '\nstatus: ' + r.status + '\n\n' + body;
} catch (e) {
out.textContent = label + '\nfetch error: ' + e;
}
}

document.getElementById('login').onclick = () =>
call('POST /api/login', '/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: document.getElementById('user').value })
});

document.getElementById('me').onclick = () => call('GET /api/me', '/api/me');
document.getElementById('count').onclick = () => call('GET /api/count', '/api/count');
document.getElementById('logout').onclick = () =>
call('POST /api/logout', '/api/logout', { method: 'POST' });
</script>
</body>
</html>
19 changes: 19 additions & 0 deletions fun/lib/http-server.fun
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
// * 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.
// * res.beforeSend(fn) runs fn(res, body) just before headers are written;
// session/cookie middleware uses it to add or clear cookies.
// * 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
Expand Down Expand Up @@ -438,6 +440,7 @@ class HttpRes()
var code = 0;
var hdrs = nil;
var sent = false;
var preSend = nil;

fun status(c)
code = c;
Expand Down Expand Up @@ -472,10 +475,26 @@ class HttpRes()
result = this;
end fun;

// Run before the response headers are written; used by session/cookie
// middleware to add or clear cookies after the handler has run.
fun beforeSend(fx)
if preSend = nil then preSend = new []; end if;
preSend.@add(fx);
result = this;
end fun;

fun send(s)
if sent then
raise 'http-server: response already sent';
end if;
if preSend <> nil then
var ps = preSend;
preSend = nil;
for i = 0 to ps.@count() - 1 do
var f = ps[i];
f(this, s);
end do;
end if;
sent = true;
if code <= 0 then
code = 200;
Expand Down
Loading
Loading