Skip to content

Commit b183c6e

Browse files
committed
refactor: decouple config generator from TUIC with XML DSL v4
Move product branding, layout, validation, field resets, random-generation metadata and export commands into XML. Replace protocol-specific runtime types and UI branches with generic state, collections and output handling. Add build-time schema selection, a non-TUIC example, native and browser reuse regressions, and updated DSL documentation and CI coverage. Validated 35 Rust tests, nightly formatting, native and WASM Clippy, TOML/JSON/YAML round trips, both browser suites and combined site assets. Real TUIC parsers accepted 72 generated configurations; local loopback SOCKS5/TUIC/TCP transport passed. Assisted-by: Codex:gpt-6
1 parent b8cf113 commit b183c6e

26 files changed

Lines changed: 1849 additions & 776 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
run: cargo clippy --workspace --all-targets --locked -- -D warnings
4545

4646
- name: Check Leptos WASM code
47-
run: cargo clippy --target wasm32-unknown-unknown --bin tuic-config-generator --locked -- -D warnings
47+
run: cargo clippy --target wasm32-unknown-unknown --bin config-generator --locked -- -D warnings
4848

4949
- name: Test XML DSL and configuration generation
5050
run: cargo test --workspace --locked
@@ -111,6 +111,14 @@ jobs:
111111
http://127.0.0.1:8765/tuic/config-generator/ > /dev/null
112112
node tests/config-generator/browser.mjs
113113
114+
- name: Verify application reuse with a different XML description
115+
env:
116+
CONFIG_SCHEMA: schema/example.xml
117+
BROWSER_CHANNEL: chromium
118+
run: |
119+
trunk --config config-generator/Trunk.toml build --release --dist "$PWD/.cache/generic-site"
120+
uv run --locked python tests/config-generator/run-browser.py --directory .cache/generic-site --script tests/config-generator/browser-generic.mjs
121+
114122
- name: Assemble Pages artifact
115123
run: |
116124
mkdir -p "$RUNNER_TEMP/pages/tuic"

‎Cargo.lock‎

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ trunk --config config-generator/Trunk.toml serve
2323
trunk --config config-generator/Trunk.toml build --release
2424
```
2525

26+
可通过 `CONFIG_SCHEMA` 指定其他 XML,路径相对 `config-generator/`(或绝对路径);默认 `schema/config.xml`。例如 PowerShell 中设置 `$env:CONFIG_SCHEMA = 'schema/example.xml'` 后构建即可生成任务清单工具,完成后用 `Remove-Item Env:CONFIG_SCHEMA` 恢复默认。详见 [DSL v4](docs/tools/config-dsl.md)。
27+
2628
产物在 `config-generator/dist/`。将整个目录交给静态服务器即可,默认使用相对资源路径,支持根路径或带结尾 `/` 的子路径。服务器需为 `.wasm` 返回 `application/wasm`;不要通过 `file://` 打开文件。若明确部署到固定前缀,可传入 `--public-url /your-prefix/`。
2729

2830
凭据使用浏览器 Crypto API 生成。所有输入、校验和序列化在本地 WASM 中完成;不加载第三方分析脚本,不保存输入、主题或凭据,不向网络提交配置。复制和下载包含明文密码,预览默认隐藏密码。
@@ -45,7 +47,7 @@ uv run --locked zensical serve
4547
cargo test --workspace --locked
4648
cargo +nightly fmt --all --check
4749
cargo clippy --workspace --all-targets --locked -- -D warnings
48-
cargo clippy --target wasm32-unknown-unknown --bin tuic-config-generator --locked -- -D warnings
50+
cargo clippy --target wasm32-unknown-unknown --bin config-generator --locked -- -D warnings
4951

5052
# 构建两个独立静态应用并放入 site/,不执行发布
5153
uv run --locked python scripts/build-site.py
@@ -80,25 +82,33 @@ uv run --locked python tests/config-generator/check-rust.py --offline
8082
npm ci --prefix tests/config-generator
8183
node tests/config-generator/browser.mjs
8284

85+
# 测试已构建的独立产物,临时本地服务随测试结束自动关闭
86+
uv run --locked python tests/config-generator/run-browser.py
87+
88+
# 组合站点构建使用部署前缀
89+
uv run --locked python tests/config-generator/run-browser.py --directory site/config-generator --prefix /tuic/config-generator/
90+
8391
# 也可以使用 Playwright 自带的 Chromium,与 CI 一致
8492
npm exec --prefix tests/config-generator -- playwright install chromium
8593
BROWSER_CHANNEL=chromium node tests/config-generator/browser.mjs
8694
```
8795

88-
`PLAYWRIGHT_MODULE_PATH` 可指定已有 Playwright 模块目录;`BROWSER_CHANNEL` 支持 `msedge`、`chrome` 和 `chromium`,CI 默认使用 `chromium`。独立 Trunk 开发服务器使用 `PREVIEW_URL=http://127.0.0.1:8080/`。测试覆盖 WASM 加载、独立页面结构、配对一致性、用户删除、TLS 切换、输入校验、转发编辑、复制下载、转义、移动端、主题、无外部请求和输入不持久化,截图在 `.cache/`。
96+
`PLAYWRIGHT_MODULE_PATH` 可指定已有 Playwright 模块目录;`BROWSER_CHANNEL` 支持 `msedge`、`chrome` 和 `chromium`,CI 默认使用 `chromium`。独立 Trunk 开发服务器使用 `PREVIEW_URL=http://127.0.0.1:8080/`。测试覆盖 WASM 加载、独立页面结构、配对一致性、用户删除、TLS 切换、输入校验、转发编辑、复制下载、转义、移动端、主题、无外部请求和输入不持久化,截图在 `.cache/`。复用检查可在以 `schema/example.xml` 构建后运行 `uv run --locked python tests/config-generator/run-browser.py --script tests/config-generator/browser-generic.mjs`;CI 将示例构建在独立临时目录,保留默认站点产物用于后续发布。
8997

9098
## DSL 与维护约定
9199

92-
[Config DSL v3](docs/tools/config-dsl.md) 使用独立的 `config-generator/schema/tuic.xml` 静态描述输入、默认值、枚举、条件、列表、映射及敏感字段,由 quick-xml + Serde 反序列化,不使用 Rust 宏或闭包编写配置描述。Leptos 读取同一份元数据生成表单;通用投影与脱敏在 `dsl.rs`,跨字段 TUIC 校验在 `validation.rs`。新增普通字段时编辑 XML、说明及相关测试。
100+
[Config DSL v4](docs/tools/config-dsl.md) 使用独立的 `config-generator/schema/config.xml` 静态描述输入、默认值、枚举、条件、列表、映射及敏感字段,由 quick-xml + Serde 反序列化,不使用 Rust 宏或闭包编写配置描述。Leptos 读取同一份元数据生成表单;通用投影与脱敏在 `dsl.rs`,通用校验与联动在 `dsl/rules.rs`,基础地址检查在 `validation.rs`。TUIC 品牌、页面分区、提示、跨字段规则、随机值生成声明、导出命令也全部由 XML 提供。新增目标应用只需更换 XML;`schema/example.xml` 提供无 TUIC 字段的复用示例。
93101

94102
| 路径 | 内容 |
95103
| --- | --- |
96104
| `Cargo.toml` / `Cargo.lock` | 生成器 Rust workspace 与锁定依赖 |
97105
| `config-generator/` | 可独立构建的 Rust + Leptos 单页应用 |
98-
| `config-generator/schema/tuic.xml` | 静态 XML 配置描述,字段、初值、条件与输出的来源 |
106+
| `config-generator/schema/config.xml` | 唯一 TUIC 产品定义:界面、字段、规则、提示与输出 |
99107
| `config-generator/src/dsl/xml.rs` / `dsl/wire.rs` / `dsl/parser.rs` | XML 子集检查、Serde 数据模型与语义校验 |
100108
| `config-generator/src/dsl.rs` | 数据投影、类型检查与脱敏 |
101-
| `config-generator/src/schema.rs` | 嵌入 XML、缓存解析结果与表单状态绑定 |
109+
| `config-generator/src/schema.rs` | 嵌入 XML、缓存解析结果、通用状态与独立行标识 |
110+
| `config-generator/src/dsl/metadata.rs` / `dsl/rules.rs` | 页面元数据、随机值声明、校验和字段联动 |
111+
| `config-generator/schema/example.xml` | 无 TUIC 字段的完整应用复用示例 |
102112
| `config-generator/src/model.rs` | 配置生成入口及三种格式序列化 |
103113
| `config-generator/tests/` | XML DSL 与配置回归测试 |
104114
| `zensical.toml` / `docs/` | 中文文档、导航、字段说明和 DSL 文档 |
@@ -113,7 +123,7 @@ BROWSER_CHANNEL=chromium node tests/config-generator/browser.mjs
113123
[CI and Pages 工作流](.github/workflows/deploy.yml) 在 PR、推送 `main` 及手动触发时运行:
114124

115125
- `check`:nightly rustfmt、stable 原生与 WASM Clippy、Rust/XML DSL 测试,以及 TOML/JSON/YAML 独立解析往返。Python 固定为 3.13,依赖使用 `uv.lock`。
116-
- `build`:使用 Trunk 0.21.14 构建文档和独立 SPA,检查站点链接及资源,再通过锁定版本的 Playwright/Chromium 运行生成器浏览器回归。Rust/Trunk、uv 和 npm 使用依赖缓存。
126+
- `build`:使用 Trunk 0.21.14 构建文档和独立 SPA,检查站点链接及资源,再通过锁定版本的 Playwright/Chromium 运行 TUIC 及无 TUIC 字段的 XML 复用浏览器回归。Rust/Trunk、uv 和 npm 使用依赖缓存。
117127
- `deploy`:依赖 `check` 和 `build` 成功,仅在 `main` 的推送或手动运行时发布;Pages 写权限和 OIDC 权限仅授予此作业,PR 只验证和构建。
118128

119129
发布产物在临时目录组装,文档位于 `/tuic/`,独立生成器位于 `/tuic/config-generator/`,根目录保留 `CNAME`。Pages 来源应设为 GitHub Actions。真实 TUIC 解析与回环测试仍按上文在具备相邻仓库的环境中运行。

‎config-generator/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "tuic-config-generator"
2+
name = "config-generator"
33
version = "0.1.0"
44
edition = "2024"
55
publish = false
@@ -9,7 +9,7 @@ serde = { version = "1", features = ["derive"] }
99
serde_json = { version = "1", features = ["preserve_order"] }
1010
toml = "0.9"
1111
uuid = "1"
12-
# v3 preserves attribute whitespace; newer XML normalization changes that contract.
12+
# The DSL preserves attribute whitespace; newer XML normalization changes that contract.
1313
quick-xml = { version = "0.38.4", features = ["serialize"] }
1414

1515
[target.'cfg(target_arch = "wasm32")'.dependencies]

‎config-generator/build.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
fn main() {
2+
println!("cargo:rerun-if-env-changed=CONFIG_SCHEMA");
3+
let root = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default());
4+
let source = std::env::var_os("CONFIG_SCHEMA")
5+
.map(std::path::PathBuf::from)
6+
.unwrap_or_else(|| root.join("schema/config.xml"));
7+
let source = if source.is_absolute() { source } else { root.join(source) };
8+
println!("cargo:rerun-if-changed={}", source.display());
9+
println!("cargo:rustc-env=CONFIG_SCHEMA_PATH={}", source.display());
10+
}

‎config-generator/examples/fixtures.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
use std::{error::Error, fs, path::PathBuf};
33

44
use serde_json::json;
5-
use tuic_config_generator::{
6-
model::{build_configs, serialize},
7-
schema::{Forward, State, User, options},
8-
};
5+
#[path = "../tests/support/mod.rs"]
6+
mod support;
7+
use support::*;
98

109
fn user(id: u64) -> User {
1110
User {

‎config-generator/index.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
6-
<meta name="description" content="在浏览器本地生成 TUIC 服务端与客户端配置。">
6+
<meta name="description" content="在浏览器本地生成配置文件。">
77
<meta name="referrer" content="no-referrer">
8-
<title>TUIC 配置生成器</title>
8+
<title>配置生成器</title>
99
<link data-trunk rel="css" href="styles.css">
10-
<link data-trunk rel="rust" data-bin="tuic-config-generator" data-wasm-opt="s" data-wasm-opt-params="--enable-bulk-memory --enable-reference-types">
10+
<link data-trunk rel="rust" data-bin="config-generator" data-wasm-opt="s" data-wasm-opt-params="--enable-bulk-memory --enable-reference-types">
1111
</head>
1212
<body>
1313
<p id="cg-loading" role="status">正在加载本地配置生成器,需要支持 WebAssembly 的现代浏览器。</p>
Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,50 @@
1-
<config-dsl version="3" target-version="2.0.0-dev4">
1+
<config-dsl version="4" target-version="2.0.0-dev4">
2+
<ui title="配置文件生成器" brand="TUIC" mark="T" eyebrow="TUIC 2.0.0-dev4 · QUINN"
3+
description="填写连接信息,生成相互匹配的服务端与客户端配置。"
4+
reference="https://docs.ihsin.dev/tuic/tools/config-generator-reference/" mode-field="mode" format-field="format"
5+
export-hint="复制和下载始终包含真实密码。配对模式请分别保存两份配置。">
6+
<section name="addresses" label="连接信息"/>
7+
<section name="authentication" label="用户认证"/>
8+
<section name="tls" label="TLS 与证书">
9+
<notice when="acme" text="域名需指向服务器,ACME HTTP-01 验证使用 TCP 80;同时开放 TUIC 的 UDP 监听端口。"/>
10+
<notice when="serverSelf" text="自签名仅用于受控测试。配对测试需明确跳过证书校验。"/>
11+
</section>
12+
<section name="local" label="本地代理" when="client"/>
13+
<section name="transport" label="高级选项" detail="日志 · 重连 · 端口转发" collapsed="true">
14+
<notice text="客户端 UDP 模式、自定义信任证书和拥塞控制暂未接入运行逻辑,本生成器不输出这些选项。"/>
15+
</section>
16+
<notice when="insecureClient" text="当前客户端配置跳过证书校验,仅适合受控测试。"/>
17+
<notice when="exposedLocal" text="SOCKS5 正在监听非回环地址且未启用认证,请确认访问范围。"/>
18+
<notice when="zeroRtt" text="0-RTT 早期数据可能被重放。"/>
19+
<notice text="此处仅检查配置字段。证书文件、DNS、防火墙和实际连接需要在部署环境验证。"/>
20+
</ui>
21+
<validators>
22+
<validator name="required" kind="required" message="请填写此项。"/>
23+
<validator name="host" kind="host" message="请输入可连接的域名或 IP,不含端口、路径或通配监听地址。"/>
24+
<validator name="port" kind="integer" min="1" max="65535" message="端口必须是 1–65535 的整数。"/>
25+
<validator name="socket" kind="socket" message="请输入有效的 IP:端口;IPv6 使用方括号。"/>
26+
<validator name="email" kind="email" message="请填写有效的邮箱地址。"/>
27+
<validator name="socks-credential" kind="length" nonblank="true" min="1" max="255" message="SOCKS5 认证字段必须为 1–255 字节。"/>
28+
<validator name="milliseconds" kind="integer" min="1" max="9007199254740991" message="请输入大于 0 的安全整数(毫秒)。"/>
29+
<validator name="seconds" kind="integer" min="1" max="9007199254740991" message="请输入大于 0 的安全整数(秒)。"/>
30+
<validator name="endpoint" kind="endpoint" message="目标必须是域名或 IP 加端口。"/>
31+
<validator name="password" kind="length" min="1" message="请输入密码,或点击生成凭据。"/>
32+
<validator name="uuid" kind="uuid" transform="trim lowercase" message="请输入非全零的标准 UUID,或点击生成凭据。"/>
33+
<validator name="domain" kind="domain" message="请填写有效的 DNS 域名。"/>
34+
<validator name="publicDomain" kind="public-domain" message="请填写公开域名。"/>
35+
<validator name="loopback" kind="loopback-socket" message="请使用回环监听地址。"/>
36+
</validators>
37+
<effects><reset on="tlsMode" target="insecure"/></effects>
38+
<rules>
39+
<assert key="sni" when="client" message="请填写证书对应的 DNS 域名作为 SNI。"><valid ref="clientSni" rule="domain"/></assert>
40+
<assert key="hostname" when="server" message="请填写有效的证书域名。"><valid ref="serverHostname" rule="domain"/></assert>
41+
<assert key="hostname" when="acme" message="ACME 需要公开域名。"><valid ref="serverHostname" rule="publicDomain"/></assert>
42+
<assert key="insecure" when="pairedSelf" message="自签名测试需要明确开启跳过证书校验,或改用受信任证书。"><truthy from="/insecure"/></assert>
43+
<assert key="maxBackoff" when="validBackoff" message="最大等待不能小于首次等待。"><compare op="gte"><source from="/maxBackoff" transform="integer"/><source from="/initialBackoff" transform="integer"/></compare></assert>
44+
<unique collection="users" key="uuid" message="UUID 重复,请为每个用户使用不同 UUID。"><source from="uuid" transform="trim lowercase"/></unique>
45+
<unique collection="forwards" key="listen" when="validBinding" message="同一协议的监听地址重复。"><source from="protocol"/><source from="listen" transform="socket"/></unique>
46+
<assert collection="forwards" key="listen" when="validPorts" message="请使用与 SOCKS5 不同的监听端口。"><compare op="ne"><source from="listen" transform="port"/><source from="/local" transform="port"/></compare></assert>
47+
</rules>
248
<inputs>
349
<field name="mode" type="enum" default="pair" section="mode" label="生成模式">
450
<option value="pair" label="配对生成" />
@@ -49,11 +95,11 @@
4995
<field name="initialBackoff" type="string" default="500" section="transport" label="首次重连等待(毫秒)" placeholder="500" hint="首次重连前等待的时间。" widget="number" when="reconnect" rule="milliseconds" />
5096
<field name="maxBackoff" type="string" default="30000" section="transport" label="最大重连等待(毫秒)" placeholder="30000" hint="应大于或等于首次等待时间。" widget="number" when="reconnect" rule="milliseconds" />
5197
<field name="activeUser" type="integer" default="0" section="internal" label="客户端使用的用户" />
52-
<collection name="users" initial-items="1">
53-
<field name="uuid" type="string" default="" label="UUID" rule="uuid" />
54-
<field name="password" type="string" default="" label="密码" widget="password" rule="password" />
98+
<collection name="users" initial-items="1" min-items="1" label="用户" section="authentication" add-label="+ 添加用户" generate-label="生成凭据" hint="配对配置自动使用相同的 UUID 和密码。" selected-by="activeUser" all-when="server" select-when="client">
99+
<field name="uuid" type="string" default="" label="UUID" rule="uuid" generator="uuid-v4" />
100+
<field name="password" type="string" default="" label="密码" widget="password" rule="password" generator="hex" bytes="24" />
55101
</collection>
56-
<collection name="forwards" initial-items="0" when="client">
102+
<collection name="forwards" initial-items="0" when="client" label="转发" section="transport" add-label="+ 添加转发" hint="服务端默认拦截回环和私有目标地址。">
57103
<field name="protocol" type="enum" default="tcp" label="转发协议">
58104
<option value="tcp" label="TCP" />
59105
<option value="udp" label="UDP" />
@@ -74,6 +120,13 @@
74120
<condition name="zeroRtt"><truthy from="/zeroRtt"/></condition>
75121
<condition name="udp"><eq from="protocol" value="udp"/></condition>
76122
<condition name="ip"><ip ref="host"/></condition>
123+
<condition name="serverSelf"><all><use ref="server"/><use ref="self"/></all></condition>
124+
<condition name="pairedSelf"><all><use ref="serverSelf"/><use ref="client"/></all></condition>
125+
<condition name="insecureClient"><all><use ref="client"/><truthy from="/insecure"/></all></condition>
126+
<condition name="exposedLocal"><all><use ref="client"/><not><truthy from="/localAuth"/></not><valid from="/local" rule="socket"/><not><valid from="/local" rule="loopback"/></not></all></condition>
127+
<condition name="validBackoff"><all><use ref="reconnect"/><valid from="/initialBackoff" rule="milliseconds"/><valid from="/maxBackoff" rule="milliseconds"/></all></condition>
128+
<condition name="validBinding"><valid from="listen" rule="socket"/></condition>
129+
<condition name="validPorts"><all><use ref="validBinding"/><valid from="/local" rule="socket"/></all></condition>
77130
</conditions>
78131
<values>
79132
<value name="host"><source from="/host" transform="trim unbracket"/></value>
@@ -82,7 +135,7 @@
82135
</values>
83136
<!-- TUIC 4719113 / Wind 9025349: literal output names, no executable code. -->
84137
<outputs>
85-
<object name="server" when="server">
138+
<object name="server" when="server" label="服务端" filename="server" command="tuic-server -c {filename}">
86139
<string name="server" from="/listen" transform="trim"/>
87140
<enum name="log_level" from="/logLevel" options="logLevel"/>
88141
<boolean name="zero_rtt_handshake" value="true" when="zeroRtt"/>
@@ -106,7 +159,7 @@
106159
</object></object>
107160
</object>
108161
</object>
109-
<object name="client" when="client">
162+
<object name="client" when="client" label="客户端" filename="client" command="tuic-client -c {filename}">
110163
<string name="server"><endpoint><source ref="host"/><source from="/port" transform="integer"/></endpoint></string>
111164
<!-- Explicit IP also avoids the baseline client's IPv6 DNS formatting issue. -->
112165
<string name="ip" ref="host" when="ip"/>

0 commit comments

Comments
 (0)