Support RSA keys and configure external ChuSQL submodules

This commit is contained in:
TheBadRoger committed 2026-10-15 20:41:23 +08:00
1 parent a631d75cee
commit d4caf05370
18 files changed
+648 -41

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
[submodule "external/ChuSQL"]
path = external/ChuSQL
url = ChuGit:/srv/git/ChuSQL.git
[submodule "external/ChuSQL-RustDriver"]
path = external/ChuSQL-RustDriver
url = ChuGit:/srv/git/ChuSQL-RustDriver.git
Generated
+369
View File
@@ -108,6 +108,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -215,6 +221,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"ssh-key",
"tempfile", "tempfile",
"tokio", "tokio",
"tower", "tower",
@@ -231,6 +238,22 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -255,6 +278,18 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -265,6 +300,16 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -272,10 +317,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle", "subtle",
] ]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.3" version = "1.0.3"
@@ -307,6 +386,16 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.14" version = "0.1.14"
@@ -375,6 +464,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
"zeroize",
] ]
[[package]] [[package]]
@@ -408,12 +498,32 @@ dependencies = [
"r-efi", "r-efi",
] ]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.5.0" version = "1.5.0"
@@ -553,6 +663,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@@ -575,6 +694,9 @@ name = "lazy_static"
version = "1.5.1" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
dependencies = [
"spin",
]
[[package]] [[package]]
name = "libc" name = "libc"
@@ -582,6 +704,12 @@ version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@@ -680,6 +808,41 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -687,6 +850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm",
] ]
[[package]] [[package]]
@@ -695,6 +859,44 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "p384"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "p521"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
"base16ct",
"ecdsa",
"elliptic-curve",
"primeorder",
"rand_core",
"sha2",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -729,6 +931,15 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -741,6 +952,27 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -763,6 +995,15 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -874,6 +1115,37 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"sha2",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.5" version = "1.1.5"
@@ -905,6 +1177,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.229" version = "1.0.229"
@@ -984,6 +1270,17 @@ dependencies = [
"unsafe-libyaml", "unsafe-libyaml",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -1009,6 +1306,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.10" version = "0.3.10"
@@ -1037,6 +1344,62 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "spin"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "ssh-cipher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
dependencies = [
"cipher",
"ssh-encoding",
]
[[package]]
name = "ssh-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
dependencies = [
"base64ct",
"pem-rfc7468",
"sha2",
]
[[package]]
name = "ssh-key"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
dependencies = [
"p256",
"p384",
"p521",
"rsa",
"sec1",
"sha2",
"signature",
"ssh-cipher",
"ssh-encoding",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -1414,6 +1777,12 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
[[package]] [[package]]
name = "zlib-rs" name = "zlib-rs"
version = "0.6.8" version = "0.6.8"
+2 -1
View File
@@ -1,3 +1,4 @@
[workspace] [workspace]
members = ["apps/api", "crates/chusql-driver"] members = ["apps/api"]
exclude = ["external/ChuSQL", "external/ChuSQL-RustDriver"]
resolver = "3" resolver = "3"
+36 -10
View File
@@ -1,18 +1,38 @@
# ChuBlog # ChuBlog
Rust/Axum 博客后端。文章作者在本地编辑 Markdown,通过受限 SSH Git 推送发布。注册、登录、权限管理使用 HTTP API;文章仅通过 Git 修改。ChuSQL 使用项目内复制的 `crates/chusql-driver`,只存可重建索引。 Rust/Axum 博客后端。文章作者在本地编辑 Markdown,通过受限 SSH Git 推送发布。注册、登录、权限管理使用 HTTP API;文章仅通过 Git 修改。ChuSQL 通过外部子模块 `external/ChuSQL-RustDriver` 接入,只存可重建索引。
本轮按最新需求只实现后端,没有前端或在线文章管理。`PLAN.md` 保留原始规划,当前范围与后续安排见 [TODO.md](TODO.md)。 本轮按最新需求只实现后端,没有前端或在线文章管理。`PLAN.md` 保留原始规划,当前范围与后续安排见 [TODO.md](TODO.md)。
## 项目与运行 ## 项目与运行
```text ```text
workspace/ ChuBlog/
├── ChuBlog/ # 本项目 ├── apps/api/
│ └── crates/chusql-driver/ # 项目内的驱动副本 ├── external/
└── ChuSQL/ # 现有数据库及已编译 Linux 产物 │ ├── ChuSQL/ # Git 子模块,主仓库只记录版本指针
│ └── ChuSQL-RustDriver/ # Git 子模块
├── config/git-local.config
└── scripts/init-deps.py
``` ```
先初始化外部依赖:
```sh
python3 scripts/init-deps.py
```
数据库地址为 `ChuGit:/srv/git/ChuSQL.git`,驱动为 `ChuGit:/srv/git/ChuSQL-RustDriver.git`。客户端需要配置 `ChuGit` SSH 别名。服务器上存在这两个本地仓库时,初始化脚本会优先使用项目级 `insteadOf` 将链接映射到 `file:///srv/git/…`,不经过 SSH。`config/git-local.config` 同时包含 SSH/GitHub 链接到本地仓库的映射,只在本项目启用,不修改全局配置。
开发电脑可明确选择已有本地仓库(工作树或 bare 仓库),不会把依赖的未提交改动复制进来:
```sh
python3 scripts/init-deps.py --database-repo ../ChuSQL --driver-repo ../ChuSQLDriver
```
子模块固定到已提交版本;源仓库必须包含这些提交。依赖源码和构建产物不进入博客仓库,更新依赖时只提交 `.gitmodules`/配置及子模块指针。不要对主仓库使用 `git add --force` 重新收录依赖文件。
开发检查: 开发检查:
```sh ```sh
@@ -25,7 +45,7 @@ cargo clippy --all-targets -- -D warnings
```sh ```sh
# ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。 # ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。
python3 scripts/prepare-deploy.py python3 scripts/prepare-deploy.py --build-database
cp .env.example .env cp .env.example .env
chmod 600 .env chmod 600 .env
# 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。 # 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。
@@ -38,7 +58,7 @@ docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
构建时若 Debian 官方下载源较慢,可在 `.env` 配置 `DEBIAN_MIRROR=mirrors.ustc.edu.cn`,仍使用 Debian 签名校验。 构建时若 Debian 官方下载源较慢,可在 `.env` 配置 `DEBIAN_MIRROR=mirrors.ustc.edu.cn`,仍使用 Debian 签名校验。
`prepare-deploy.py` 复制相邻 ChuSQL 的既有编译产物,不修改该项目;没有产物会明确失败。跨平台部署须先在目标 Debian/架构构建,不能复制 macOS、Windows 或其他架构二进制。镜像不含编译器、源码、`.env` 或密码文件。首次启动从环境中的哈希创建管理员,后续账号与权限以持久文件为准,修改初始哈希不会覆盖已有账户。 `prepare-deploy.py` 默认从数据库子模块读取编译产物;`--build-database` 会先构建其 Rust 存储库、Haskell server 和 bootstrap。也可用 `--chusql-source /已有构建的/ChuSQL` 选择已有外部构建,没有产物会明确失败。编译产物只暂存在被忽略的 `deploy/bin/`。跨平台部署须先在目标 Debian/架构构建,不能复制 macOS、Windows 或其他架构二进制。镜像不含编译器、源码、`.env` 或密码文件。首次启动从环境中的哈希创建管理员,后续账号与权限以持久文件为准,修改初始哈希不会覆盖已有账户。
Caddy 根据 `SITE_ADDRESS` 提供 HTTPS 和 API 反向代理;`PUBLIC_ORIGIN` 必须与外部站点完全一致,不带结尾斜杠。HTTP 仅对 localhost 开发场景允许。只需公网 TCP 80/443 和 Git SSH 2222(可改);主机管理 SSH 另行管理,数据库和 API 不映射宿主端口。既有 UDP 7001 不受影响。参考 [Caddy 请求体限制](https://caddyserver.com/docs/caddyfile/directives/request_body) 与 [反向代理文档](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)。 Caddy 根据 `SITE_ADDRESS` 提供 HTTPS 和 API 反向代理;`PUBLIC_ORIGIN` 必须与外部站点完全一致,不带结尾斜杠。HTTP 仅对 localhost 开发场景允许。只需公网 TCP 80/443 和 Git SSH 2222(可改);主机管理 SSH 另行管理,数据库和 API 不映射宿主端口。既有 UDP 7001 不受影响。参考 [Caddy 请求体限制](https://caddyserver.com/docs/caddyfile/directives/request_body) 与 [反向代理文档](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)。
@@ -48,7 +68,7 @@ Caddy 根据 `SITE_ADDRESS` 提供 HTTPS 和 API 反向代理;`PUBLIC_ORIGIN`
1. 注册:`POST /api/auth/register`,正文 `{ "username": "alice", "password": "至少12字节的口令" }`。默认角色 `reader`,绝不接受客户端指定角色。`ALLOW_REGISTRATION=false` 可关闭注册。 1. 注册:`POST /api/auth/register`,正文 `{ "username": "alice", "password": "至少12字节的口令" }`。默认角色 `reader`,绝不接受客户端指定角色。`ALLOW_REGISTRATION=false` 可关闭注册。
2. 登录:`POST /api/auth/login`,相同字段。浏览器保存 HttpOnly Cookie;返回 `csrf`。所有修改请求必须携带站点 `Origin` 和 `X-CSRF-Token`。 2. 登录:`POST /api/auth/login`,相同字段。浏览器保存 HttpOnly Cookie;返回 `csrf`。所有修改请求必须携带站点 `Origin` 和 `X-CSRF-Token`。
3. 本地生成 `ssh-keygen -t ed25519`;登录后 `PUT /api/account/ssh-key` 提交 `{ "public_key": "ssh-ed25519 AAAA..." }`,只交公钥。 3. 本地生成 `ssh-keygen -t ed25519` 或 `ssh-keygen -t rsa -b 3072`;登录后 `PUT /api/account/ssh-key` 提交 `{ "public_key": "ssh-ed25519 AAAA..." }`,只交公钥。RSA 使用标准 `ssh-rsa AAAA...` 公钥文本,支持 2048–8192 位;实际认证使用 RSA/SHA-256 或 RSA/SHA-512,不启用旧的 SHA-1 签名。见 [OpenSSH 说明](https://www.openssh.com/txt/release-8.8)。
4. 管理员登录后调用 `PATCH /api/admin/users/alice`,正文 `{ "enabled": true, "role": "publisher", "prefixes": ["alice-"] }`。不同有效发布者的前缀不能重叠。 4. 管理员登录后调用 `PATCH /api/admin/users/alice`,正文 `{ "enabled": true, "role": "publisher", "prefixes": ["alice-"] }`。不同有效发布者的前缀不能重叠。
5. 此时 SSH 才接受该用户的密钥;`reader`、停用用户、未绑密钥的用户均不能访问 Git。管理员也需显式分配前缀和公钥才能发布。 5. 此时 SSH 才接受该用户的密钥;`reader`、停用用户、未绑密钥的用户均不能访问 Git。管理员也需显式分配前缀和公钥才能发布。
@@ -99,7 +119,7 @@ MVP 每次只接受 **一个直接继承远端 main 的提交**,首次仅允
- Markdown 上限 256 KiB;图片单张 2 MiB、最多 12 张、尺寸 4096×4096;文章合计 6 MiB;快照原始文件合计 64 MiB、最多 1000 篇。 - Markdown 上限 256 KiB;图片单张 2 MiB、最多 12 张、尺寸 4096×4096;文章合计 6 MiB;快照原始文件合计 64 MiB、最多 1000 篇。
- Git 输入 pack 上限 8 MiB;接收仓库达到 256 MiB 后拒绝新 SSH 会话,需要管理员离线维护。此阈值是应用检查,生产还应为 Docker 数据盘配置真实文件系统配额。 - Git 输入 pack 上限 8 MiB;接收仓库达到 256 MiB 后拒绝新 SSH 会话,需要管理员离线维护。此阈值是应用检查,生产还应为 Docker 数据盘配置真实文件系统配额。
- 使用成熟 Markdown 解析器,拒绝原始 HTML、危险链接协议、外链图片、控制符和双向隐藏字符;代码块是惰性文本,绝不执行 MDX、JS、仓库脚本、构建任务或上传内容。 - 使用成熟 Markdown 解析器,拒绝原始 HTML、危险链接协议、外链图片、控制符和双向隐藏字符;代码块是惰性文本,绝不执行 MDX、JS、仓库脚本、构建任务或上传内容。
- 仅支持 Ed25519 公钥;拒绝把 `authorized_keys` 选项当作公钥上传。SSH 只接受精确的 `git-receive-pack 'content.git'` / `git-upload-pack 'content.git'`,没有 shell、SCP/SFTP、TTY、转发、隧道、用户环境变量。 - 支持 Ed25519 和 2048–8192 位 RSA 公钥,拒绝弱密钥、负数/畸形 RSA 参数及额外二进制数据;拒绝把 `authorized_keys` 选项当作公钥上传。SSH 只接受精确的 `git-receive-pack 'content.git'` / `git-upload-pack 'content.git'`,没有 shell、SCP/SFTP、TTY、转发、隧道、用户环境变量。
- 接收仓库由 UID 10002 管理;API 为独立 UID 10001,正式仓库/用户密码文件不挂载给 SSH 容器;SSH 只读授权文件和只读固定 hook。接收内容再次验证后才导入 API 自己的 Git 仓库。 - 接收仓库由 UID 10002 管理;API 为独立 UID 10001,正式仓库/用户密码文件不挂载给 SSH 容器;SSH 只读授权文件和只读固定 hook。接收内容再次验证后才导入 API 自己的 Git 仓库。
- HTTP 请求体 8 KiB;注册/登录合计全局 10 次/分钟,修改操作全局 60 次/分钟;密码哈希并发最多 2。1000 用户、1000 个有效会话上限,一小时固定会话期限。全局限制偏保守,后续可细化 IP/账户限制以改善公平性。 - HTTP 请求体 8 KiB;注册/登录合计全局 10 次/分钟,修改操作全局 60 次/分钟;密码哈希并发最多 2。1000 用户、1000 个有效会话上限,一小时固定会话期限。全局限制偏保守,后续可细化 IP/账户限制以改善公平性。
- 统计不收集 IP/访客身份:公开文章数、标签数、Markdown 字节数、启动以来文章 API 请求数。请求数不是独立访客数,重启后清零。 - 统计不收集 IP/访客身份:公开文章数、标签数、Markdown 字节数、启动以来文章 API 请求数。请求数不是独立访客数,重启后清零。
@@ -119,8 +139,14 @@ MVP 每次只接受 **一个直接继承远端 main 的提交**,首次仅允
## 联调 ## 联调
```sh ```sh
python3 scripts/prepare-deploy.py python3 scripts/prepare-deploy.py --build-database
python3 scripts/smoke.py python3 scripts/smoke.py
python3 scripts/smoke.py --key-type rsa
``` ```
联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。覆盖真实 SSH 身份与授权、非法推送、发布撤回、ChuSQL 索引、断库阅读、重启与撤权。该测试会拉取基础镜像,需要 Docker 可用。 联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。覆盖真实 SSH 身份与授权、非法推送、发布撤回、ChuSQL 索引、断库阅读、重启与撤权。该测试会拉取基础镜像,需要 Docker 可用。
## 历史清理后的协作
驱动的旧源码副本会从主仓库历史中移除,后续只保留外部子模块配置。历史重写后,已有克隆建议重新克隆并初始化依赖;不要把旧 main 合并回来,否则可能重新引入已删除的依赖历史。自建服务器的不可达对象清理与其他人的旧克隆需要分别处理,GitHub 缓存和旧提交直链的回收由平台管理。
+2 -2
View File
@@ -7,13 +7,13 @@
- [x] Rust/Axum API:公开文章列表、详情、图片、基础统计、健康接口。 - [x] Rust/Axum API:公开文章列表、详情、图片、基础统计、健康接口。
- [x] 用户注册、登录、退出、修改密码;Argon2id、服务端会话、Cookie、Origin/CSRF 校验。 - [x] 用户注册、登录、退出、修改密码;Argon2id、服务端会话、Cookie、Origin/CSRF 校验。
- [x] reader / publisher / admin 权限;注册不能自选权限,管理员单独审批发布权、停用用户,修改权限使已有会话失效。 - [x] reader / publisher / admin 权限;注册不能自选权限,管理员单独审批发布权、停用用户,修改权限使已有会话失效。
- [x] 用户上传/轮换自己的 Ed25519 公钥,公钥存在与具有发布权分别检查。 - [x] 用户上传/轮换自己的 Ed25519/RSA 公钥,公钥存在与具有发布权分别检查。
- [x] OpenSSH 强制命令:仅固定仓库的 Git push/clone;禁用 shell、SFTP/SCP、TTY、代理/端口转发、隧道与用户环境。 - [x] OpenSSH 强制命令:仅固定仓库的 Git push/clone;禁用 shell、SFTP/SCP、TTY、代理/端口转发、隧道与用户环境。
- [x] 发布者 slug 前缀授权,不允许重叠授权或修改其他命名空间。 - [x] 发布者 slug 前缀授权,不允许重叠授权或修改其他命名空间。
- [x] pre-receive 校验 Git quarantine 内的候选提交:单次一个线性提交、仅 main、禁止删除分支、强推、标签及其他分支。 - [x] pre-receive 校验 Git quarantine 内的候选提交:单次一个线性提交、仅 main、禁止删除分支、强推、标签及其他分支。
- [x] Markdown Frontmatter、路径、文件模式、大小配额、图片类型/内容/尺寸与链接安全校验;拒绝原始 HTML、隐藏控制符、符号链接、可执行文件及额外文件。 - [x] Markdown Frontmatter、路径、文件模式、大小配额、图片类型/内容/尺寸与链接安全校验;拒绝原始 HTML、隐藏控制符、符号链接、可执行文件及额外文件。
- [x] 独立 SSH 接收仓库与 API 正式仓库;从 Git 恢复公开快照,草稿不出现在公开 API/资源接口。 - [x] 独立 SSH 接收仓库与 API 正式仓库;从 Git 恢复公开快照,草稿不出现在公开 API/资源接口。
- [x] 项目内 `crates/chusql-driver` 副本;ChuSQL 公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。 - [x] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置与本地路径优先映射;ChuSQL 公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。
- [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。 - [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。
- [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。 - [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。
+2 -1
View File
@@ -9,13 +9,14 @@ argon2 = "0.5"
axum = "0.8" axum = "0.8"
base64 = "0.22" base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
chusql-driver = { path = "../../crates/chusql-driver" } chusql-driver = { path = "../../external/ChuSQL-RustDriver" }
fs2 = "0.4" fs2 = "0.4"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
rand = "0.8" rand = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_yaml = "0.9" serde_yaml = "0.9"
ssh-key = { version = "0.6.7", default-features = false, features = ["std"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
pulldown-cmark = "0.13" pulldown-cmark = "0.13"
tracing = "0.1" tracing = "0.1"
+31 -19
View File
@@ -10,10 +10,7 @@ use axum::{
http::{HeaderMap, HeaderValue, StatusCode, header}, http::{HeaderMap, HeaderValue, StatusCode, header},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use base64::{ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
Engine,
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
};
use rand::RngCore; use rand::RngCore;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -428,23 +425,38 @@ pub async fn logout(State(app): State<Arc<App>>, headers: HeaderMap) -> ApiResul
} }
pub fn public_key(value: &str) -> Result<String> { pub fn public_key(value: &str) -> Result<String> {
ensure!( ensure!(
value.len() < 512 && !value.contains(['\n', '\r']), value.len() <= 4096 && !value.contains(['\n', '\r']),
"公钥格式无效" "公钥格式无效"
); );
let parts: Vec<_> = value.split_whitespace().collect(); let key = ssh_key::PublicKey::from_openssh(value)
ensure!( .context("SSH 公钥格式无效,禁止 authorized_keys 选项")?;
parts.len() >= 2 && parts[0] == "ssh-ed25519", match key.key_data() {
"仅支持 Ed25519 公钥,禁止 authorized_keys 选项" ssh_key::public::KeyData::Ed25519(_) => {}
); ssh_key::public::KeyData::Rsa(rsa) => {
let bytes = STANDARD.decode(parts[1])?; let modulus = rsa.n.as_positive_bytes().context("RSA 模数必须为正数")?;
ensure!( let first = modulus.first().context("RSA 模数为空")?;
bytes.len() == 51 let bits = modulus.len() * 8 - first.leading_zeros() as usize;
&& bytes[..4] == [0, 0, 0, 11] ensure!(
&& &bytes[4..15] == b"ssh-ed25519" (2048..=8192).contains(&bits) && modulus.last().is_some_and(|b| b & 1 == 1),
&& bytes[15..19] == [0, 0, 0, 32], "RSA 模数必须为 2048–8192 位正奇数"
"Ed25519 公钥内容无效" );
); let exponent = rsa.e.as_positive_bytes().context("RSA 指数必须为正数")?;
Ok(format!("ssh-ed25519 {}", STANDARD.encode(bytes))) ensure!(exponent.len() <= 4, "RSA 指数过大");
let exponent = exponent.iter().fold(0_u32, |n, b| (n << 8) | u32::from(*b));
ensure!(
exponent >= 3 && exponent % 2 == 1,
"RSA 指数必须为至少 3 的奇数"
);
}
_ => anyhow::bail!("仅支持 Ed25519 或 RSA 公钥"),
}
// Drop comments and options: the stored value is exactly the parsed public key.
Ok(format!(
"{} {}",
key.algorithm().as_str(),
key.to_bytes()
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes))?
))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
+52
View File
@@ -253,6 +253,58 @@ fn ssh_keys_cannot_inject_options_or_commands() {
assert!(accounts::public_key(&format!("{key}\nssh-rsa AAA")).is_err()); assert!(accounts::public_key(&format!("{key}\nssh-rsa AAA")).is_err());
assert!(accounts::public_key("ssh-ed25519 AAAA").is_err()); assert!(accounts::public_key("ssh-ed25519 AAAA").is_err());
} }
fn rsa_key(bits: usize, exponent: &[u8]) -> String {
let mut modulus = vec![0xff; bits.div_ceil(8)];
modulus[0] >>= (8 - bits % 8) % 8;
if modulus[0] & 0x80 != 0 {
modulus.insert(0, 0);
}
let mut bytes = Vec::new();
for field in [b"ssh-rsa".as_slice(), exponent, modulus.as_slice()] {
bytes.extend((field.len() as u32).to_be_bytes());
bytes.extend(field);
}
format!(
"ssh-rsa {}",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
}
#[test]
fn rsa_keys_enforce_strength_encoding_and_safe_canonical_form() {
for bits in [2048, 3072, 4096, 8192] {
let key = rsa_key(bits, &[1, 0, 1]);
assert_eq!(
accounts::public_key(&format!("{key} user comment")).unwrap(),
key
);
}
for key in [
rsa_key(1024, &[1, 0, 1]),
rsa_key(2047, &[1, 0, 1]),
rsa_key(8193, &[1, 0, 1]),
rsa_key(2048, &[2]),
rsa_key(2048, &[1]),
rsa_key(2048, &[0x80]),
rsa_key(2048, &[1, 0, 0, 0, 1]),
] {
assert!(accounts::public_key(&key).is_err());
}
let key = rsa_key(2048, &[1, 0, 1]);
assert!(accounts::public_key(&key.replace("ssh-rsa", "ssh-ed25519")).is_err());
assert!(accounts::public_key(&format!("command=\"sh\" {key}")).is_err());
let encoded = key.split_whitespace().nth(1).unwrap();
let mut bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.unwrap();
bytes.push(0);
assert!(
accounts::public_key(&format!(
"ssh-rsa {}",
base64::engine::general_purpose::STANDARD.encode(bytes)
))
.is_err()
);
}
async fn app(dir: &std::path::Path) -> Arc<App> { async fn app(dir: &std::path::Path) -> Arc<App> {
let config = dir.join("publishers.json"); let config = dir.join("publishers.json");
let hash = accounts::hash_password("Administrator-test-123".into()) let hash = accounts::hash_password("Administrator-test-123".into())
+13
View File
@@ -0,0 +1,13 @@
# Optional on the self-hosted server; loaded by scripts/init-deps.py when these repos exist.
[url "file:///srv/git/ChuSQL.git"]
insteadOf = ChuGit:/srv/git/ChuSQL.git
insteadOf = ssh://git@chunana.cn/srv/git/ChuSQL.git
insteadOf = git@chunana.cn:/srv/git/ChuSQL.git
insteadOf = git@github.com:TheBadRoger/ChuSQL.git
insteadOf = https://github.com/TheBadRoger/ChuSQL.git
[url "file:///srv/git/ChuSQL-RustDriver.git"]
insteadOf = ChuGit:/srv/git/ChuSQL-RustDriver.git
insteadOf = ssh://git@chunana.cn/srv/git/ChuSQL-RustDriver.git
insteadOf = git@chunana.cn:/srv/git/ChuSQL-RustDriver.git
insteadOf = git@github.com:TheBadRoger/ChuSQL-RustDriver.git
insteadOf = https://github.com/TheBadRoger/ChuSQL-RustDriver.git
+2 -1
View File
@@ -8,7 +8,8 @@ KbdInteractiveAuthentication no
PermitEmptyPasswords no PermitEmptyPasswords no
PubkeyAuthentication yes PubkeyAuthentication yes
AuthenticationMethods publickey AuthenticationMethods publickey
PubkeyAcceptedAlgorithms ssh-ed25519 PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
RequiredRSASize 2048
AllowUsers git AllowUsers git
AuthorizedKeysFile none AuthorizedKeysFile none
AuthorizedKeysCommand /usr/local/bin/chublog-authorized-keys AuthorizedKeysCommand /usr/local/bin/chublog-authorized-keys
+1 -1
View File
@@ -13,7 +13,7 @@
| POST | `/api/auth/login` | 公共 + Origin;`{username,password}`,返回 `{username,role,csrf}` 及 Cookie | | POST | `/api/auth/login` | 公共 + Origin;`{username,password}`,返回 `{username,role,csrf}` 及 Cookie |
| POST | `/api/auth/logout` | 登录 + Origin + CSRF;使会话失效并清除 Cookie | | POST | `/api/auth/logout` | 登录 + Origin + CSRF;使会话失效并清除 Cookie |
| GET | `/api/account` | 登录;返回自己的 `{username,permissions,csrf}`,不含口令哈希 | | GET | `/api/account` | 登录;返回自己的 `{username,permissions,csrf}`,不含口令哈希 |
| PUT | `/api/account/ssh-key` | 登录 + Origin + CSRF;`{public_key}`,替换自己的唯一 Ed25519 公钥 | | PUT | `/api/account/ssh-key` | 登录 + Origin + CSRF;`{public_key}`,替换自己的唯一 Ed25519 或 RSA 公钥(RSA 2048–8192 位) |
| PUT | `/api/account/password` | 登录 + Origin + CSRF;`{old_password,new_password}`,成功后全部会话失效 | | PUT | `/api/account/password` | 登录 + Origin + CSRF;`{old_password,new_password}`,成功后全部会话失效 |
| GET | `/api/admin/users` | 管理员;返回用户启用状态、角色、前缀及公钥,不含哈希 | | GET | `/api/admin/users` | 管理员;返回用户启用状态、角色、前缀及公钥,不含哈希 |
| PATCH | `/api/admin/users/:username` | 管理员 + Origin + CSRF;`{enabled,role,prefixes}` | | PATCH | `/api/admin/users/:username` | 管理员 + Origin + CSRF;`{enabled,role,prefixes}` |
@@ -0,0 +1,13 @@
# ADR-002:外部项目使用子模块配置
## 状态
已接受,2026-10-15。替代早期复制数据库驱动源码的做法。
## 决定
数据库服务与 Rust 驱动作为两个独立 Git 子模块维护。主仓库只提交远程地址、固定版本指针、初始化/构建配置,不提交依赖源码或编译产物。Cargo 引用初始化后的驱动路径,数据库从外部 checkout 构建并暂存到被忽略的 Docker 构建目录。
正式来源使用 `ChuGit:/srv/git/ChuSQL.git` 与 `ChuGit:/srv/git/ChuSQL-RustDriver.git`。在服务器可读本地 bare 仓库时,项目级 `insteadOf` 优先使用 `file:///srv/git/…`;开发者可显式指定已有本地来源。文件协议只在初始化这两个明确子模块的命令中启用,不设置全局许可,不递归获取未知子模块。
已有驱动副本从主仓库的历史树中删除。外部项目仍各自保留完整历史;主仓库不能替代依赖仓库分发其对象,源仓库必须先具有固定指针所引用的提交。
Vendored Submodule
+1
Submodule external/ChuSQL added at a45e33f94b.
Vendored Submodule
+1
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Initialize pinned external submodules, preferring local repositories over SSH."""
import argparse
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parents[1]
PROJECTS = ['ChuSQL', 'ChuSQL-RustDriver']
PREFIX = 'ChuGit:/srv/git/'
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--database-repo', type=Path, help='local clone/bare repository of ChuSQL')
parser.add_argument('--driver-repo', type=Path, help='local clone/bare repository of ChuSQL-RustDriver')
args = parser.parse_args()
git_args = []
local_config = ROOT / 'config/git-local.config'
if not args.database_repo and not args.driver_repo and all((Path('/srv/git') / f'{name}.git').is_dir() for name in PROJECTS):
current = subprocess.run(['git', 'config', '--local', '--get-all', 'include.path'], cwd=ROOT, text=True, capture_output=True, check=False)
if str(local_config) not in current.stdout.splitlines():
subprocess.run(['git', 'config', '--local', '--add', 'include.path', str(local_config)], cwd=ROOT, check=True)
git_args.extend(['-c', f'include.path={local_config}'])
for name, override in zip(PROJECTS, [args.database_repo, args.driver_repo]):
source = override or Path('/srv/git') / f'{name}.git'
if override and not source.is_dir():
parser.error(f'Local repository does not exist: {source}')
if source.is_dir():
source = source.resolve()
subprocess.run(['git', '-C', str(source), 'rev-parse', '--git-dir'], check=True, capture_output=True)
key = f'url.{source.as_uri()}.insteadOf'
url = f'{PREFIX}{name}.git'
subprocess.run(['git', 'config', '--local', '--replace-all', key, url], cwd=ROOT, check=True)
git_args.extend(['-c', f'{key}={url}'])
print(f'{name}: using local repository {source}', flush=True)
# The file protocol is enabled only for this explicit, fixed set of dependencies.
if git_args:
git_args.extend(['-c', 'protocol.file.allow=always'])
subprocess.run(['git', *git_args, 'submodule', 'update', '--init', '--', *[f'external/{name}' for name in PROJECTS]], cwd=ROOT, check=True)
if __name__ == '__main__':
main()
+15 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build the API and copy existing local ChuSQL release artifacts into the Docker context.""" """Build the API and stage external ChuSQL artifacts without committing dependencies."""
import argparse import argparse
from pathlib import Path from pathlib import Path
import shutil import shutil
@@ -8,17 +8,27 @@ import subprocess
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--no-build', action='store_true', help='use an existing API release binary') parser.add_argument('--no-build', action='store_true', help='use an existing API release binary')
parser.add_argument('--chusql-source', type=Path, default=root / 'external/ChuSQL', help='initialized/built external ChuSQL checkout')
parser.add_argument('--build-database', action='store_true', help='build external database artifacts before staging')
args = parser.parse_args() args = parser.parse_args()
chusql = args.chusql_source.resolve()
if not (root / 'external/ChuSQL-RustDriver/Cargo.toml').is_file():
parser.error('Initialize external dependencies with scripts/init-deps.py first.')
if not (chusql / 'chusql-core/storage/Cargo.toml').is_file():
parser.error(f'ChuSQL source is missing: {chusql}; initialize dependencies first.')
if not args.no_build: if not args.no_build:
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=root, check=True) subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=root, check=True)
out = root / 'deploy' / 'bin' if args.build_database:
subprocess.run(['cargo', 'build', '--locked', '--release'], cwd=chusql / 'chusql-core/storage', check=True)
for package in ['chusql-server', 'chusql-bootstrap']:
subprocess.run(['stack', 'build', '--fast'], cwd=chusql / package, check=True)
out = root / 'deploy/bin'
out.mkdir(parents=True, exist_ok=True) out.mkdir(parents=True, exist_ok=True)
shutil.copy2(root / 'target/release/chublog-api', out / 'chublog-api') shutil.copy2(root / 'target/release/chublog-api', out / 'chublog-api')
chusql = root.parent / 'ChuSQL'
for package, executable in [('chusql-server', 'chusql-server'), ('chusql-bootstrap', 'csql-bootstrap')]: for package, executable in [('chusql-server', 'chusql-server'), ('chusql-bootstrap', 'csql-bootstrap')]:
matches = list((chusql / package / '.stack-work/install').glob(f'*/**/bin/{executable}')) matches = list((chusql / package / '.stack-work/install').glob(f'*/**/bin/{executable}'))
if len(matches) != 1: if len(matches) != 1:
raise SystemExit(f'Expected one built {executable}, found {len(matches)}; build {package} first using its stack.yaml.') raise SystemExit(f'Expected one built {executable}, found {len(matches)}; use --build-database or select a built --chusql-source.')
shutil.copy2(matches[0], out / executable) shutil.copy2(matches[0], out / executable)
shutil.copy2(chusql / 'chusql-core/storage/target/release/libchusql_core_storage.so', out) shutil.copy2(chusql / 'chusql-core/storage/target/release/libchusql_core_storage.so', out)
print('Prepared deploy/bin. Build and run Compose from this same Linux architecture (Debian 13).') print('Prepared ignored deploy/bin. Build and run Compose on the same Debian 13 architecture.')
+5 -1
View File
@@ -3,6 +3,7 @@
Uses an isolated Compose project and removes only that project's containers/volumes. Uses an isolated Compose project and removes only that project's containers/volumes.
Run prepare-deploy.py first. Ports 18080, 18443, 12222 must be free. Run prepare-deploy.py first. Ports 18080, 18443, 12222 must be free.
""" """
import argparse
import http.cookiejar import http.cookiejar
import json import json
import os import os
@@ -16,6 +17,9 @@ import urllib.request
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
ORIGIN = 'http://localhost:18080' ORIGIN = 'http://localhost:18080'
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--key-type', choices=['ed25519', 'rsa'], default='ed25519')
args = parser.parse_args()
def run(args, **kwargs): def run(args, **kwargs):
@@ -77,7 +81,7 @@ with tempfile.TemporaryDirectory(prefix='chublog-smoke-') as temporary:
publisher = Client() publisher = Client()
publisher.call('POST', '/api/auth/register', {'username': 'alice', 'password': password}) publisher.call('POST', '/api/auth/register', {'username': 'alice', 'password': password})
publisher.csrf = publisher.call('POST', '/api/auth/login', {'username': 'alice', 'password': password})['csrf'] publisher.csrf = publisher.call('POST', '/api/auth/login', {'username': 'alice', 'password': password})['csrf']
run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(tmp / 'key')]) run(['ssh-keygen', '-q', '-t', args.key_type, '-b', '3072', '-N', '', '-f', str(tmp / 'key')])
publisher.call('PUT', '/api/account/ssh-key', {'public_key': (tmp / 'key.pub').read_text().strip()}) publisher.call('PUT', '/api/account/ssh-key', {'public_key': (tmp / 'key.pub').read_text().strip()})
publisher.call('GET', '/api/admin/users', expected=403) publisher.call('GET', '/api/admin/users', expected=403)
env = os.environ.copy() env = os.environ.copy()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Verify pinned submodules initialize from local repositories without an SSH connection."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
SCRIPT = Path(__file__).with_name('init-deps.py')
class InitializeDependencies(unittest.TestCase):
def test_local_rewrite_fetches_only_pinned_dependency_commits(self):
with tempfile.TemporaryDirectory(prefix='chublog deps ') as temporary:
root = Path(temporary)
project = root / 'blog'
project.mkdir()
(project / 'scripts').mkdir()
shutil.copy2(SCRIPT, project / 'scripts/init-deps.py')
env = {**os.environ, 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_CONFIG_NOSYSTEM': '1'}
def git(cwd, *args):
return subprocess.run(['git', *args], cwd=cwd, env=env, check=True, text=True, capture_output=True).stdout.strip()
git(project, 'init', '--initial-branch=main')
modules = []
sources = []
for name in ['ChuSQL', 'ChuSQL-RustDriver']:
source = root / name
source.mkdir()
git(source, 'init', '--initial-branch=main')
(source / 'source.txt').write_text(f'{name} committed source')
git(source, 'add', '.')
git(source, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-m', 'Dependency version')
oid = git(source, 'rev-parse', 'HEAD')
# Dirty source must never leak into the blog or fetched dependency.
(source / 'source.txt').write_text('uncommitted changes')
path = f'external/{name}'
modules.append(f'[submodule "{path}"]\n path = {path}\n url = ChuGit:/srv/git/{name}.git\n')
git(project, 'update-index', '--add', '--cacheinfo', f'160000,{oid},{path}')
sources.append(str(source))
(project / '.gitmodules').write_text(''.join(modules))
git(project, 'add', '.gitmodules')
subprocess.run(['python3', 'scripts/init-deps.py', '--database-repo', sources[0], '--driver-repo', sources[1]], cwd=project, env=env, check=True, capture_output=True)
for name in ['ChuSQL', 'ChuSQL-RustDriver']:
self.assertEqual((project / f'external/{name}/source.txt').read_text(), f'{name} committed source')
self.assertTrue(all(line.startswith('160000') for line in git(project, 'ls-files', '--stage', 'external').splitlines()))
# Running the initialization a second time must preserve the same pins.
subprocess.run(['python3', 'scripts/init-deps.py', '--database-repo', sources[0], '--driver-repo', sources[1]], cwd=project, env=env, check=True, capture_output=True)
if __name__ == '__main__':
unittest.main()