mirror of
https://github.com/DefectingCat/candy
synced 2025-07-15 16:51:34 +00:00
Compare commits
15 Commits
v0.2.1
...
47d8482ba6
Author | SHA1 | Date | |
---|---|---|---|
47d8482ba6 | |||
d59b02fae2 | |||
fffcee1d50 | |||
25ff344474 | |||
13c4570e20 | |||
e92b191360 | |||
14196c937c | |||
091f0c8eb5 | |||
f96e440264 | |||
36cd153fab | |||
f0da74a06e | |||
a5041197c7 | |||
515b36176d | |||
46c2f90f92 | |||
64a85dd5a4 |
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,13 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.1
|
||||
## 0.2.2 - 2025-07-03
|
||||
|
||||
Features:
|
||||
|
||||
- Support lua script
|
||||
- Add max body size limit
|
||||
|
||||
## 0.2.1 - 2025-06-24
|
||||
|
||||
Features:
|
||||
|
||||
- `auto-index` support
|
||||
- Stable rust version
|
||||
|
||||
## 0.2.0
|
||||
## 0.2.0 - 2025-06-17
|
||||
|
||||
Features:
|
||||
|
||||
@ -15,7 +22,7 @@ Features:
|
||||
- Refactor with axum
|
||||
- SSL support
|
||||
|
||||
## 0.1.1
|
||||
## 0.1.1 - 2024-07-02
|
||||
|
||||
Features:
|
||||
|
||||
@ -35,7 +42,7 @@ Fix:
|
||||
- Custom error page
|
||||
- Config tests
|
||||
|
||||
## 0.1.0
|
||||
## 0.1.0 - 2024-05-13
|
||||
|
||||
Features:
|
||||
|
||||
|
208
Cargo.lock
generated
208
Cargo.lock
generated
@ -316,7 +316,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
@ -324,10 +324,10 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"syn",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -366,6 +366,16 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
@ -386,7 +396,7 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "candy"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@ -405,6 +415,7 @@ dependencies = [
|
||||
"md5",
|
||||
"mimalloc",
|
||||
"mime_guess",
|
||||
"mlua",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -654,12 +665,28 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.11"
|
||||
@ -1190,6 +1217,17 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@ -1221,6 +1259,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@ -1318,6 +1365,25 @@ version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "547.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.5.12+a4f56a4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"which 7.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -1335,9 +1401,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@ -1396,6 +1462,55 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"either",
|
||||
"erased-serde",
|
||||
"futures-util",
|
||||
"mlua-sys",
|
||||
"mlua_derive",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde-value",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"lua-src",
|
||||
"luajit-src",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua_derive"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870d71c172fcf491c6b5fb4c04160619a2ee3e5a42a1402269c66bcbf1dd4deb"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
@ -1511,6 +1626,15 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@ -1583,6 +1707,28 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.93"
|
||||
@ -1662,9 +1808,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.20"
|
||||
version = "0.12.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.22.1",
|
||||
@ -1730,6 +1876,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@ -1857,6 +2009,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
@ -2107,17 +2269,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.1"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
@ -2339,6 +2503,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
@ -2549,6 +2719,18 @@ dependencies = [
|
||||
"rustix 0.38.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix 1.0.5",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -2732,6 +2914,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
|
18
Cargo.toml
18
Cargo.toml
@ -2,14 +2,14 @@
|
||||
|
||||
[package]
|
||||
name = "candy"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# core
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
tokio = { version = "1.46.0", features = ["full"] }
|
||||
tokio-util = "0.7.15"
|
||||
tokio-rustls = "0.26.2"
|
||||
hyper = { version = "1.6.0", features = ["full"] }
|
||||
@ -23,7 +23,7 @@ axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
tower = { version = "0.5.2", features = ["full"] }
|
||||
tower-http = { version = "0.6.6", features = ["full"] }
|
||||
reqwest = { version = "0.12.20", features = [
|
||||
reqwest = { version = "0.12.22", features = [
|
||||
# "rustls-tls",
|
||||
"native-tls-vendored",
|
||||
"zstd",
|
||||
@ -32,6 +32,16 @@ reqwest = { version = "0.12.20", features = [
|
||||
'deflate',
|
||||
"stream",
|
||||
] }
|
||||
# lua
|
||||
mlua = { version = "0.10.5", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"send",
|
||||
"async",
|
||||
"macros",
|
||||
"serialize",
|
||||
"anyhow",
|
||||
] }
|
||||
# tools
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.98"
|
||||
@ -39,7 +49,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.23"
|
||||
bytes = "1.10.1"
|
||||
const_format = "0.2.34"
|
||||
md5 = "0.7.0"
|
||||
md5 = "0.8.0"
|
||||
dashmap = { version = "6.1.0", features = ["serde"] }
|
||||
# logging
|
||||
tracing = "0.1.41"
|
||||
|
15
README.md
15
README.md
@ -1,11 +1,26 @@
|
||||
# Candy
|
||||
|
||||
<img src="./assets/candy-transparent.png" width="200px">
|
||||
|
||||
A tiny web server built with rust.
|
||||
|
||||
[](https://deps.rs/repo/github/DefectingCat/candy)
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Simple and easy to use
|
||||
- Single binary
|
||||
- Supports SSL
|
||||
- HTTP 2 support
|
||||
- Supports lua script
|
||||
- List directory
|
||||
|
||||
## TODO
|
||||
|
||||
[TODO.md](./TODO.md)
|
||||
|
||||
## Changelog
|
||||
|
||||
[CHANGELOG.md](./CHANGELOG.md)
|
||||
|
BIN
assets/candy-transparent.png
Normal file
BIN
assets/candy-transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 KiB |
BIN
assets/candy.png
Normal file
BIN
assets/candy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
@ -1,7 +1,7 @@
|
||||
# Virtual host
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 4000
|
||||
port = 80
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# SSL certificate
|
||||
@ -11,11 +11,9 @@ timeout = 15
|
||||
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./html/selfsigned.key -out ./html/selfsigned.crt
|
||||
certificate = "./html/selfsigned.crt"
|
||||
certificate_key = "./html/selfsigned.key"
|
||||
|
||||
# Add custom headers to response
|
||||
[host.headers]
|
||||
X-Powered-By = "candy"
|
||||
|
||||
# Routes for virtual host
|
||||
[[host.route]]
|
||||
# Route location
|
||||
@ -23,12 +21,11 @@ location = "/"
|
||||
# Static file root
|
||||
# or proxy_pass
|
||||
# or redirect
|
||||
root = "./html"
|
||||
root = "html"
|
||||
# Only use for root field
|
||||
index = ["index.html"]
|
||||
# List directory
|
||||
auto_index = true
|
||||
|
||||
# Custom 500 page
|
||||
[host.route.error_page]
|
||||
status = 500
|
||||
@ -38,8 +35,22 @@ page = "500.html"
|
||||
status = 404
|
||||
page = "404.html"
|
||||
|
||||
# Reverse proxy
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/proxy/"
|
||||
location = "/"
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
# Client request max body size (bytes)
|
||||
max_body_size = 2048
|
||||
|
||||
# Lua script
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8081
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
lua_script = "html/index.lua"
|
||||
|
@ -6,4 +6,78 @@ title: 配置文件
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
||||
Candy 遵循配置文件进行配置。配置文件的格式为 TOML。
|
||||
|
||||
### 虚拟主机
|
||||
|
||||
顶层配置为虚拟主机 `host`,可以配置多个虚拟主机。
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 80
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# 只用当 ssl = true 时,才会读取证书和密钥,并开启 SSL 支持
|
||||
# ssl = true
|
||||
# Self sign a certificate
|
||||
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./html/selfsigned.key -out ./html/selfsigned.crt
|
||||
certificate = "./html/selfsigned.crt"
|
||||
certificate_key = "./html/selfsigned.key"
|
||||
```
|
||||
|
||||
#### 自定义 HTTP 相应头
|
||||
|
||||
每个虚拟主机都可以配置自定义相应头
|
||||
|
||||
TODO
|
||||
|
||||
### 路由
|
||||
|
||||
每个虚拟主机下都可以配置多个路由。配置字段为 `route`。
|
||||
|
||||
每个路由支持三种配置:
|
||||
|
||||
- 静态文件托管
|
||||
- 反向代理
|
||||
- Lua 脚本
|
||||
|
||||
#### 静态文件托管
|
||||
|
||||
```toml
|
||||
[[host.route]]
|
||||
# 路由地址
|
||||
location = "/"
|
||||
# 静态文件根目录
|
||||
root = "html"
|
||||
# 当使用静态文件根目录时,使用下面的字段作为主页
|
||||
index = ["index.html"]
|
||||
# 列出目录
|
||||
auto_index = true
|
||||
```
|
||||
|
||||
#### 反向代理
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
# Client request max body size (bytes)
|
||||
max_body_size = 2048
|
||||
```
|
||||
|
||||
#### Lua 脚本
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8081
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
lua_script = "html/index.lua"
|
||||
```
|
||||
|
@ -23,3 +23,11 @@ Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
只需要一个可执行文件和一个配置文件,就可以快速部署一个 HTTP 服务器。
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -c config.toml
|
||||
```
|
||||
|
||||
`-c` 可以省略,当省略时,默认使用当前目录下的 `config.toml` 文件。
|
||||
|
@ -5,9 +5,10 @@ import { themes as prismThemes } from 'prism-react-renderer';
|
||||
const config: Config = {
|
||||
title: 'Candy',
|
||||
tagline: 'Tiny http web server',
|
||||
favicon: 'img/favicon.ico',
|
||||
favicon: 'img/candy-transparent.png',
|
||||
|
||||
future: {
|
||||
v4: true,
|
||||
experimental_faster: true,
|
||||
},
|
||||
|
||||
@ -66,8 +67,8 @@ const config: Config = {
|
||||
navbar: {
|
||||
title: 'RUA',
|
||||
logo: {
|
||||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
alt: 'Candy Logo',
|
||||
src: 'img/candy-transparent.png',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
|
@ -45,7 +45,7 @@
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "Switch between dark and light mode (currently {mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
"description": "The ARIA label for the color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "dark mode",
|
||||
@ -309,5 +309,17 @@
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "Tags",
|
||||
"description": "The title of the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.system": {
|
||||
"message": "system mode",
|
||||
"description": "The name for the system color mode"
|
||||
},
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": {
|
||||
"message": "Expand the dropdown",
|
||||
"description": "The ARIA label of the button to expand the mobile dropdown navbar item"
|
||||
},
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": {
|
||||
"message": "Collapse the dropdown",
|
||||
"description": "The ARIA label of the button to collapse the mobile dropdown navbar item"
|
||||
}
|
||||
}
|
||||
|
@ -7,3 +7,79 @@ title: Config File
|
||||
## Config File
|
||||
|
||||
Candy follows the config file to configure.
|
||||
|
||||
### Virtual Host
|
||||
|
||||
The top level configuration is the virtual host `host`, and can configure multiple virtual hosts.
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 80
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# Only read certificate and key when ssl = true, and enable SSL support
|
||||
# ssl = true
|
||||
# Self sign a certificate
|
||||
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./html/selfsigned.key -out ./html/selfsigned.crt
|
||||
certificate = "./html/selfsigned.crt"
|
||||
certificate_key = "./html/selfsigned.key"
|
||||
```
|
||||
|
||||
#### Custom HTTP Response Header
|
||||
|
||||
Each virtual host can configure custom response header.
|
||||
|
||||
TODO
|
||||
|
||||
### Route
|
||||
|
||||
Each virtual host can configure multiple routes. The configuration field is `route`.
|
||||
|
||||
Each route supports three configurations:
|
||||
|
||||
- Static file hosting
|
||||
- Reverse proxy
|
||||
- Lua script
|
||||
|
||||
#### Static File Hosting
|
||||
|
||||
```toml
|
||||
[[host.route]]
|
||||
# Route location
|
||||
location = "/"
|
||||
# Static file root
|
||||
# or proxy_pass
|
||||
# or redirect
|
||||
root = "html"
|
||||
# Only use for root field
|
||||
index = ["index.html"]
|
||||
# List directory
|
||||
auto_index = true
|
||||
```
|
||||
|
||||
#### Reverse Proxy
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
# Client request max body size (bytes)
|
||||
max_body_size = 2048
|
||||
```
|
||||
|
||||
#### Lua Script
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8081
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
lua_script = "html/index.lua"
|
||||
```
|
||||
|
@ -23,3 +23,11 @@ Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
Only one config file is supported, the default config file is `./config.toml`.
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -c config.toml
|
||||
```
|
||||
|
||||
`-c` can be omitted, and when omitted, the default config file is `./config.toml` in the current directory.
|
||||
|
@ -45,7 +45,7 @@
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "切换浅色/暗黑模式(当前为{mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
"description": "The ARIA label for the color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "暗黑模式",
|
||||
@ -309,5 +309,17 @@
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "标签",
|
||||
"description": "The title of the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.system": {
|
||||
"message": "system mode",
|
||||
"description": "The name for the system color mode"
|
||||
},
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": {
|
||||
"message": "Expand the dropdown",
|
||||
"description": "The ARIA label of the button to expand the mobile dropdown navbar item"
|
||||
},
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": {
|
||||
"message": "Collapse the dropdown",
|
||||
"description": "The ARIA label of the button to collapse the mobile dropdown navbar item"
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,78 @@ title: 配置文件
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
||||
Candy 遵循配置文件进行配置。配置文件的格式为 TOML。
|
||||
|
||||
### 虚拟主机
|
||||
|
||||
顶层配置为虚拟主机 `host`,可以配置多个虚拟主机。
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 80
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# 只用当 ssl = true 时,才会读取证书和密钥,并开启 SSL 支持
|
||||
# ssl = true
|
||||
# Self sign a certificate
|
||||
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./html/selfsigned.key -out ./html/selfsigned.crt
|
||||
certificate = "./html/selfsigned.crt"
|
||||
certificate_key = "./html/selfsigned.key"
|
||||
```
|
||||
|
||||
#### 自定义 HTTP 相应头
|
||||
|
||||
每个虚拟主机都可以配置自定义相应头
|
||||
|
||||
TODO
|
||||
|
||||
### 路由
|
||||
|
||||
每个虚拟主机下都可以配置多个路由。配置字段为 `route`。
|
||||
|
||||
每个路由支持三种配置:
|
||||
|
||||
- 静态文件托管
|
||||
- 反向代理
|
||||
- Lua 脚本
|
||||
|
||||
#### 静态文件托管
|
||||
|
||||
```toml
|
||||
[[host.route]]
|
||||
# 路由地址
|
||||
location = "/"
|
||||
# 静态文件根目录
|
||||
root = "html"
|
||||
# 当使用静态文件根目录时,使用下面的字段作为主页
|
||||
index = ["index.html"]
|
||||
# 列出目录
|
||||
auto_index = true
|
||||
```
|
||||
|
||||
#### 反向代理
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8080
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
# Client request max body size (bytes)
|
||||
max_body_size = 2048
|
||||
```
|
||||
|
||||
#### Lua 脚本
|
||||
|
||||
```toml
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 8081
|
||||
[[host.route]]
|
||||
location = "/"
|
||||
lua_script = "html/index.lua"
|
||||
```
|
||||
|
@ -23,3 +23,11 @@ Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
只需要一个可执行文件和一个配置文件,就可以快速部署一个 HTTP 服务器。
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -c config.toml
|
||||
```
|
||||
|
||||
`-c` 可以省略,当省略时,默认使用当前目录下的 `config.toml` 文件。
|
||||
|
@ -19,22 +19,22 @@
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.6.3",
|
||||
"@docusaurus/faster": "^3.6.3",
|
||||
"@docusaurus/preset-classic": "3.6.3",
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/faster": "^3.8.1",
|
||||
"@docusaurus/preset-classic": "3.8.1",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.6.3",
|
||||
"@docusaurus/tsconfig": "3.6.3",
|
||||
"@docusaurus/types": "3.6.3",
|
||||
"prettier": "^3.4.2",
|
||||
"@docusaurus/module-type-aliases": "3.8.1",
|
||||
"@docusaurus/tsconfig": "3.8.1",
|
||||
"@docusaurus/types": "3.8.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -51,5 +51,5 @@
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.2"
|
||||
"packageManager": "pnpm@10.12.4"
|
||||
}
|
||||
|
3091
docs/pnpm-lock.yaml
generated
3091
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -14,30 +14,22 @@ const FeatureList: FeatureItem[] = [
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus was designed from the ground up to be easily installed and
|
||||
used to get your website up and running quickly.
|
||||
Single executable binary, with a TOML config file, quick to deploy a
|
||||
http server.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Focus on What Matters',
|
||||
title: 'Performance',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||
ahead and move your docs into the <code>docs</code> directory.
|
||||
</>
|
||||
<>Multiple threads, asynchronous I/O, and multi-platform support.</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Powered by React',
|
||||
title: 'Powered by Rust',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend or customize your website layout by reusing React. Docusaurus can
|
||||
be extended while reusing the same header and footer.
|
||||
</>
|
||||
),
|
||||
description: <>Built with Rust, axum and tokio.</>,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -21,7 +21,7 @@ function HomepageHeader() {
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro"
|
||||
>
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
Quick start - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,10 +32,7 @@ function HomepageHeader() {
|
||||
export default function Home(): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<Layout title={`${siteConfig.title}`} description="Candy, http web server">
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
|
BIN
docs/static/img/candy-transparent.png
vendored
Normal file
BIN
docs/static/img/candy-transparent.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 KiB |
BIN
docs/static/img/candy.png
vendored
Normal file
BIN
docs/static/img/candy.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
@ -40,6 +40,11 @@ pub struct SettingRoute {
|
||||
/// Timeout for connect to upstream
|
||||
#[serde(default = "upstream_timeout_default")]
|
||||
pub proxy_timeout: u16,
|
||||
/// Request max body size (bytes)
|
||||
pub max_body_size: Option<u64>,
|
||||
|
||||
/// Lua script
|
||||
pub lua_script: Option<String>,
|
||||
}
|
||||
|
||||
/// Host routes
|
||||
|
@ -5,7 +5,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use const_format::{concatcp, formatcp};
|
||||
use const_format::formatcp;
|
||||
use serde_repr::*;
|
||||
use tracing::error;
|
||||
|
||||
@ -36,7 +36,7 @@ pub enum ErrorCode {
|
||||
}
|
||||
|
||||
/// Normal error message
|
||||
const SERVER_ERROR_STR: &str = concatcp!(
|
||||
const SERVER_ERROR_STR: &str = formatcp!(
|
||||
r#"Internal Server Error
|
||||
{NAME} v{VERSION}
|
||||
Powered by RUA
|
||||
|
149
src/http/lua.rs
Normal file
149
src/http/lua.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Request},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
|
||||
use mlua::{UserData, UserDataMethods, UserDataRef};
|
||||
use tokio::fs::{self};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
http::{HOSTS, LUA_ENGINE, error::RouteError, serve::resolve_parent_path},
|
||||
utils::parse_port_from_host,
|
||||
};
|
||||
|
||||
use super::error::RouteResult;
|
||||
|
||||
/// 为 Lua 脚本提供 HTTP 请求上下文
|
||||
#[derive(Clone, Debug)]
|
||||
struct CandyRequest {
|
||||
#[allow(dead_code)]
|
||||
method: String,
|
||||
/// Uri 在路由中被添加到上下文中
|
||||
uri: Uri,
|
||||
}
|
||||
/// 为 Lua 脚本提供 HTTP 响应上下文
|
||||
#[derive(Clone, Debug)]
|
||||
struct CandyResponse {
|
||||
status: u16,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
}
|
||||
// HTTP 请求上下文,可在 Lua 中使用
|
||||
#[derive(Clone, Debug)]
|
||||
struct RequestContext {
|
||||
req: CandyRequest,
|
||||
res: CandyResponse,
|
||||
}
|
||||
|
||||
impl UserData for RequestContext {
|
||||
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
|
||||
// 获取请求路径
|
||||
methods.add_method("get_path", |_, this, ()| {
|
||||
Ok(this.req.uri.path().to_string())
|
||||
});
|
||||
|
||||
// 获取请求方法
|
||||
methods.add_method("get_method", |_, this, ()| Ok(this.req.method.to_string()));
|
||||
|
||||
// 设置响应状态码
|
||||
methods.add_method_mut("set_status", |_, this, status: u16| {
|
||||
this.res.status = status;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// 设置响应内容
|
||||
methods.add_method_mut("set_body", |_, this, body: String| {
|
||||
this.res.body = format!("{}{}", this.res.body, body);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// 设置响应头
|
||||
methods.add_method_mut("set_header", |_, this, (key, value): (String, String)| {
|
||||
this.res.headers.insert(
|
||||
HeaderName::from_str(&key).map_err(|err| anyhow!("header name error: {err}"))?,
|
||||
HeaderValue::from_str(&value)
|
||||
.map_err(|err| anyhow!("header value error: {err}"))?,
|
||||
);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lua(
|
||||
req_uri: Uri,
|
||||
path: Option<Path<String>>,
|
||||
Host(host): Host,
|
||||
req: Request<Body>,
|
||||
) -> RouteResult<impl IntoResponse> {
|
||||
let scheme = req.uri().scheme_str().unwrap_or("http");
|
||||
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
|
||||
let route_map = &HOSTS
|
||||
.get(&port)
|
||||
.ok_or(RouteError::BadRequest())
|
||||
.with_context(|| {
|
||||
format!("Hosts not found for port: {port}, host: {host}, scheme: {scheme}")
|
||||
})?
|
||||
.route_map;
|
||||
tracing::debug!("Route map entries: {:?}", route_map);
|
||||
|
||||
let parent_path = resolve_parent_path(&req_uri, path.as_ref());
|
||||
let route_config = route_map
|
||||
.get(&parent_path)
|
||||
.ok_or(RouteError::RouteNotFound())
|
||||
.with_context(|| format!("route not found: {parent_path}"))?;
|
||||
let lua_script = route_config
|
||||
.lua_script
|
||||
.as_ref()
|
||||
.ok_or(RouteError::InternalError())
|
||||
.with_context(|| "lua script not found")?;
|
||||
|
||||
let method = req.method().to_string();
|
||||
|
||||
let lua = &LUA_ENGINE.lua;
|
||||
let script = fs::read_to_string(lua_script)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read lua script file: {lua_script}",))?;
|
||||
lua.globals()
|
||||
.set(
|
||||
"ctx",
|
||||
RequestContext {
|
||||
req: CandyRequest {
|
||||
method,
|
||||
uri: req_uri,
|
||||
},
|
||||
res: CandyResponse {
|
||||
status: 200,
|
||||
headers: HeaderMap::new(),
|
||||
body: "".to_string(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
error!("Lua script {lua_script} exec error: {err}");
|
||||
RouteError::InternalError()
|
||||
})?;
|
||||
lua.load(script).exec_async().await.map_err(|err| {
|
||||
error!("Lua script {lua_script} exec error: {err}");
|
||||
RouteError::InternalError()
|
||||
})?;
|
||||
// 获取修改后的上下文并返回响应
|
||||
let ctx: UserDataRef<RequestContext> = lua.globals().get("ctx").map_err(|err| {
|
||||
error!("Lua script {lua_script} exec error: {err}");
|
||||
RouteError::InternalError()
|
||||
})?;
|
||||
let res = ctx.res.clone();
|
||||
|
||||
let mut response = Response::builder();
|
||||
let body = Body::from(res.body);
|
||||
response = response.status(res.status);
|
||||
let response = response
|
||||
.body(body)
|
||||
.with_context(|| "Failed to build HTTP response with lua")?;
|
||||
Ok(response)
|
||||
}
|
213
src/http/mod.rs
213
src/http/mod.rs
@ -1,15 +1,21 @@
|
||||
use std::{net::SocketAddr, sync::LazyLock, time::Duration};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{Router, middleware, routing::get};
|
||||
use axum::{Router, extract::DefaultBodyLimit, middleware, routing::get};
|
||||
use axum_server::{Handle, tls_rustls::RustlsConfig};
|
||||
use dashmap::DashMap;
|
||||
use mlua::Lua;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{compression::CompressionLayer, timeout::TimeoutLayer};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
config::SettingHost,
|
||||
consts::{ARCH, COMMIT, COMPILER, NAME, OS, VERSION},
|
||||
middlewares::{add_headers, add_version, logging_route},
|
||||
utils::graceful_shutdown,
|
||||
};
|
||||
@ -19,6 +25,8 @@ pub mod error;
|
||||
pub mod serve;
|
||||
// handle reverse proxy
|
||||
pub mod reverse_proxy;
|
||||
// handle lua script
|
||||
pub mod lua;
|
||||
|
||||
/// Host configuration
|
||||
/// use virtual host port as key
|
||||
@ -33,6 +41,80 @@ pub mod reverse_proxy;
|
||||
/// }
|
||||
pub static HOSTS: LazyLock<DashMap<u16, SettingHost>> = LazyLock::new(DashMap::new);
|
||||
|
||||
pub struct LuaEngine {
|
||||
pub lua: Lua,
|
||||
/// Lua 共享字典
|
||||
#[allow(dead_code)]
|
||||
pub shared_table: Arc<DashMap<String, String>>,
|
||||
}
|
||||
impl LuaEngine {
|
||||
pub fn new() -> Self {
|
||||
let lua = Lua::new();
|
||||
let shared_table: DashMap<String, String> = DashMap::new();
|
||||
let shared_table = Arc::new(shared_table);
|
||||
|
||||
let module = lua.create_table().expect("create table failed");
|
||||
let shared_api = lua.create_table().expect("create shared table failed");
|
||||
|
||||
// 创建共享字典到 lua 中
|
||||
let shared_table_get = shared_table.clone();
|
||||
shared_api
|
||||
.set(
|
||||
"set",
|
||||
lua.create_function(move |_, (key, value): (String, String)| {
|
||||
shared_table_get.insert(key, value.clone());
|
||||
Ok(())
|
||||
})
|
||||
.expect("create set function failed"),
|
||||
)
|
||||
.expect("set failed");
|
||||
let shared_table_get = shared_table.clone();
|
||||
shared_api
|
||||
.set(
|
||||
"get",
|
||||
lua.create_function(move |_, key: String| {
|
||||
let t = shared_table_get.get(&key).ok_or(anyhow!("key not found"))?;
|
||||
Ok(t.clone())
|
||||
})
|
||||
.expect("create get function failed"),
|
||||
)
|
||||
.expect("get failed");
|
||||
module
|
||||
.set("shared", shared_api)
|
||||
.expect("set shared_api failed");
|
||||
|
||||
// 日志函数
|
||||
module
|
||||
.set(
|
||||
"log",
|
||||
lua.create_function(move |_, msg: String| {
|
||||
tracing::info!("Lua: {}", msg);
|
||||
Ok(())
|
||||
})
|
||||
.expect("create log function failed"),
|
||||
)
|
||||
.expect("set log failed");
|
||||
|
||||
module.set("version", VERSION).expect("set version failed");
|
||||
module.set("name", NAME).expect("set name failed");
|
||||
module.set("os", OS).expect("set os failed");
|
||||
module.set("arch", ARCH).expect("set arch failed");
|
||||
module
|
||||
.set("compiler", COMPILER)
|
||||
.expect("set compiler failed");
|
||||
module.set("commit", COMMIT).expect("set commit failed");
|
||||
|
||||
// 全局变量 candy
|
||||
lua.globals()
|
||||
.set("candy", module)
|
||||
.expect("set candy table to lua engine failed");
|
||||
|
||||
Self { lua, shared_table }
|
||||
}
|
||||
}
|
||||
/// lua 脚本执行器
|
||||
pub static LUA_ENGINE: LazyLock<LuaEngine> = LazyLock::new(LuaEngine::new);
|
||||
|
||||
pub async fn make_server(host: SettingHost) -> anyhow::Result<()> {
|
||||
let mut router = Router::new();
|
||||
let host_to_save = host.clone();
|
||||
@ -40,72 +122,95 @@ pub async fn make_server(host: SettingHost) -> anyhow::Result<()> {
|
||||
// convert to axum routes
|
||||
// register routes
|
||||
for host_route in &host.route {
|
||||
// reverse proxy
|
||||
if host_route.proxy_pass.is_some() {
|
||||
router = router.route(host_route.location.as_ref(), get(reverse_proxy::serve));
|
||||
// register wildcard path /doc/*
|
||||
// lua script
|
||||
if host_route.lua_script.is_some() {
|
||||
// papare lua script
|
||||
router = router.route(host_route.location.as_ref(), get(lua::lua));
|
||||
let route_path = format!("{}{{*path}}", host_route.location);
|
||||
router = router.route(route_path.as_ref(), get(reverse_proxy::serve));
|
||||
router = router.route(route_path.as_ref(), get(lua::lua));
|
||||
// save route path to map
|
||||
{
|
||||
host_to_save
|
||||
.route_map
|
||||
.insert(host_route.location.clone(), host_route.clone());
|
||||
}
|
||||
} else {
|
||||
// static file
|
||||
if host_route.root.is_none() {
|
||||
warn!("root field not found for route: {:?}", host_route.location);
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// reverse proxy
|
||||
if host_route.proxy_pass.is_some() {
|
||||
router = router.route(host_route.location.as_ref(), get(reverse_proxy::serve));
|
||||
// register wildcard path /doc/*
|
||||
let route_path = format!("{}{{*path}}", host_route.location);
|
||||
router = router.route(route_path.as_ref(), get(reverse_proxy::serve));
|
||||
// Set request max body size
|
||||
if let Some(max_body_size) = host_route.max_body_size {
|
||||
router = router.layer(DefaultBodyLimit::max(max_body_size as usize));
|
||||
}
|
||||
// resister with location
|
||||
// location = "/doc"
|
||||
// route: GET /doc/*
|
||||
// resister with file path
|
||||
// index = ["index.html", "index.txt"]
|
||||
// route: GET /doc/index.html
|
||||
// route: GET /doc/index.txt
|
||||
// register parent path /doc
|
||||
let path_morethan_one = host_route.location.len() > 1;
|
||||
let route_path = if path_morethan_one && host_route.location.ends_with('/') {
|
||||
// first register path with slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
let len = host_route.location.len();
|
||||
let path_without_slash = host_route.location.chars().collect::<Vec<_>>()
|
||||
[0..len - 1]
|
||||
.iter()
|
||||
.collect::<String>();
|
||||
// then register path without slash /doc/
|
||||
router = router.route(&path_without_slash, get(serve::serve));
|
||||
debug!("registed route {}", path_without_slash);
|
||||
host_route.location.clone()
|
||||
} else if path_morethan_one {
|
||||
// first register path without slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
// then register path with slash /doc/
|
||||
let path = format!("{}/", host_route.location);
|
||||
router = router.route(&path, get(serve::serve));
|
||||
debug!("registed route {}", path);
|
||||
path
|
||||
} else {
|
||||
// register path /doc/
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
host_route.location.clone()
|
||||
};
|
||||
// save route path to map
|
||||
{
|
||||
host_to_save
|
||||
.route_map
|
||||
.insert(route_path.clone(), host_route.clone());
|
||||
.insert(host_route.location.clone(), host_route.clone());
|
||||
}
|
||||
let route_path = format!("{route_path}{{*path}}");
|
||||
// register wildcard path /doc/*
|
||||
router = router.route(route_path.as_ref(), get(serve::serve));
|
||||
debug!("registed route: {}", route_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// static file
|
||||
if host_route.root.is_none() {
|
||||
warn!("root field not found for route: {:?}", host_route.location);
|
||||
continue;
|
||||
}
|
||||
// Set request max body size
|
||||
if let Some(max_body_size) = host_route.max_body_size {
|
||||
router = router.layer(DefaultBodyLimit::max(max_body_size as usize));
|
||||
}
|
||||
// resister with location
|
||||
// location = "/doc"
|
||||
// route: GET /doc/*
|
||||
// resister with file path
|
||||
// index = ["index.html", "index.txt"]
|
||||
// route: GET /doc/index.html
|
||||
// route: GET /doc/index.txt
|
||||
// register parent path /doc
|
||||
let path_morethan_one = host_route.location.len() > 1;
|
||||
let route_path = if path_morethan_one && host_route.location.ends_with('/') {
|
||||
// first register path with slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
let len = host_route.location.len();
|
||||
let path_without_slash = host_route.location.chars().collect::<Vec<_>>()[0..len - 1]
|
||||
.iter()
|
||||
.collect::<String>();
|
||||
// then register path without slash /doc/
|
||||
router = router.route(&path_without_slash, get(serve::serve));
|
||||
debug!("registed route {}", path_without_slash);
|
||||
host_route.location.clone()
|
||||
} else if path_morethan_one {
|
||||
// first register path without slash /doc
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
// then register path with slash /doc/
|
||||
let path = format!("{}/", host_route.location);
|
||||
router = router.route(&path, get(serve::serve));
|
||||
debug!("registed route {}", path);
|
||||
path
|
||||
} else {
|
||||
// register path /doc/
|
||||
router = router.route(&host_route.location, get(serve::serve));
|
||||
debug!("registed route {}", host_route.location);
|
||||
host_route.location.clone()
|
||||
};
|
||||
// save route path to map
|
||||
{
|
||||
host_to_save
|
||||
.route_map
|
||||
.insert(route_path.clone(), host_route.clone());
|
||||
}
|
||||
let route_path = format!("{route_path}{{*path}}");
|
||||
// register wildcard path /doc/*
|
||||
router = router.route(route_path.as_ref(), get(serve::serve));
|
||||
debug!("registed route: {}", route_path);
|
||||
}
|
||||
|
||||
// save host to map
|
||||
|
@ -157,21 +157,30 @@ pub async fn serve(
|
||||
|
||||
let scheme = req.uri().scheme_str().unwrap_or("http");
|
||||
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
|
||||
let route_map = &HOSTS.get(&port).ok_or(RouteError::BadRequest())?.route_map;
|
||||
let route_map = &HOSTS
|
||||
.get(&port)
|
||||
.ok_or(RouteError::BadRequest())
|
||||
.with_context(|| {
|
||||
format!("Hosts not found for port: {port}, host: {host}, scheme: {scheme}")
|
||||
})?
|
||||
.route_map;
|
||||
tracing::debug!("Route map entries: {:?}", route_map);
|
||||
|
||||
let parent_path = resolve_parent_path(&req_uri, path.as_ref());
|
||||
tracing::debug!("parent path: {:?}", parent_path);
|
||||
let proxy_config = route_map
|
||||
.get(&parent_path)
|
||||
.ok_or(RouteError::RouteNotFound())?;
|
||||
.ok_or(RouteError::RouteNotFound())
|
||||
.with_context(|| format!("route not found: {parent_path}"))?;
|
||||
tracing::debug!("proxy pass: {:?}", proxy_config);
|
||||
let Some(ref proxy_pass) = proxy_config.proxy_pass else {
|
||||
return handle_custom_page(proxy_config, req, true).await;
|
||||
};
|
||||
let uri = format!("{proxy_pass}{path_query}");
|
||||
tracing::debug!("reverse proxy uri: {:?}", &uri);
|
||||
*req.uri_mut() = Uri::try_from(uri.clone()).map_err(|_| RouteError::InternalError())?;
|
||||
*req.uri_mut() = Uri::try_from(uri.clone())
|
||||
.map_err(|_| RouteError::InternalError())
|
||||
.with_context(|| format!("uri not found: {uri}"))?;
|
||||
|
||||
let timeout = proxy_config.proxy_timeout;
|
||||
|
||||
@ -188,7 +197,6 @@ pub async fn serve(
|
||||
|
||||
// forward request body
|
||||
let body = req.into_body();
|
||||
// TODO: set body size limit
|
||||
let bytes = axum::body::to_bytes(body, 2048).await.map_err(|err| {
|
||||
tracing::error!("Failed to proxy request: {}", err);
|
||||
RouteError::InternalError()
|
||||
@ -211,7 +219,8 @@ pub async fn serve(
|
||||
reqwest_response.headers(),
|
||||
response_builder
|
||||
.headers_mut()
|
||||
.ok_or(RouteError::InternalError())?,
|
||||
.ok_or(RouteError::InternalError())
|
||||
.with_context(|| "headers not found")?,
|
||||
);
|
||||
let res = response_builder
|
||||
.body(Body::from_stream(reqwest_response.bytes_stream()))
|
||||
|
@ -15,7 +15,7 @@ use axum_extra::extract::Host;
|
||||
use dashmap::mapref::one::Ref;
|
||||
use http::{
|
||||
HeaderMap, HeaderValue, StatusCode, Uri,
|
||||
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION},
|
||||
};
|
||||
use mime_guess::from_path;
|
||||
use tokio::fs::{self, File};
|
||||
@ -52,30 +52,34 @@ async fn custom_page(
|
||||
host_route
|
||||
.error_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
.ok_or(RouteError::RouteNotFound())
|
||||
.with_context(|| "error page not found")?
|
||||
} else {
|
||||
host_route
|
||||
.not_found_page
|
||||
.as_ref()
|
||||
.ok_or(RouteError::RouteNotFound())?
|
||||
.ok_or(RouteError::RouteNotFound())
|
||||
.with_context(|| "not found page not found")?
|
||||
};
|
||||
|
||||
let root = host_route
|
||||
.root
|
||||
.as_ref()
|
||||
.ok_or(RouteError::InternalError())?;
|
||||
.ok_or(RouteError::InternalError())
|
||||
.with_context(|| "root not found")?;
|
||||
|
||||
let path = format!("{}/{}", root, page.page);
|
||||
|
||||
let status = StatusCode::from_str(page.status.to_string().as_ref())
|
||||
.map_err(|_| RouteError::BadRequest())?;
|
||||
.map_err(|_| RouteError::BadRequest())
|
||||
.with_context(|| format!("status code not found: {}", page.status))?;
|
||||
|
||||
tracing::debug!("custom not found path: {:?}", path);
|
||||
|
||||
match stream_file(path.into(), request, Some(status)).await {
|
||||
Ok(res) => RouteResult::Ok(res),
|
||||
Err(e) => {
|
||||
println!("Failed to stream file: {:?}", e);
|
||||
error!("Failed to stream file: {:?}", e);
|
||||
RouteResult::Err(RouteError::InternalError())
|
||||
}
|
||||
}
|
||||
@ -125,11 +129,18 @@ pub async fn serve(
|
||||
// which is `host_route.location`
|
||||
let scheme = request.uri().scheme_str().unwrap_or("http");
|
||||
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
|
||||
let route_map = &HOSTS.get(&port).ok_or(RouteError::BadRequest())?.route_map;
|
||||
let route_map = &HOSTS
|
||||
.get(&port)
|
||||
.ok_or(RouteError::BadRequest())
|
||||
.with_context(|| {
|
||||
format!("Hosts not found for port: {port}, host: {host}, scheme: {scheme}")
|
||||
})?
|
||||
.route_map;
|
||||
debug!("Route map entries: {:?}", route_map);
|
||||
let host_route = route_map
|
||||
.get(&parent_path)
|
||||
.ok_or(RouteError::RouteNotFound())?;
|
||||
.ok_or(RouteError::RouteNotFound())
|
||||
.with_context(|| format!("route not found: {parent_path}"))?;
|
||||
debug!("route: {:?}", host_route);
|
||||
// after route found
|
||||
// check static file root configuration
|
||||
@ -149,7 +160,7 @@ pub async fn serve(
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
let path = path.to_string();
|
||||
if path.contains('.') {
|
||||
(root.into(), vec![format!("{}/{}", root, path)])
|
||||
(root.into(), vec![format!("{}{}", root, path)])
|
||||
} else {
|
||||
generate_default_index(&host_route, &format!("{root}/{path}"))
|
||||
}
|
||||
@ -157,6 +168,7 @@ pub async fn serve(
|
||||
generate_default_index(&host_route, root)
|
||||
};
|
||||
debug!("request index file {:?}", path_arr);
|
||||
debug!("req_path: {:?}", req_path);
|
||||
// Try each candidate path in order:
|
||||
// - Return the first successfully streamed file.
|
||||
// - If all fail, return a `RouteNotFound` error.
|
||||
@ -172,6 +184,26 @@ pub async fn serve(
|
||||
let path_exists = match path_exists {
|
||||
Some(path_exists) => path_exists,
|
||||
None => {
|
||||
let uri_path = uri.path();
|
||||
// 如果请求路径不以 / 结尾,则返回 301 Moved Permanently 状态码
|
||||
if !uri_path.ends_with('/') {
|
||||
let mut response = Response::builder();
|
||||
let stream = empty_stream().await?;
|
||||
let body = Body::from_stream(stream);
|
||||
response
|
||||
.headers_mut()
|
||||
.with_context(|| "insert header failed")?
|
||||
.insert(
|
||||
LOCATION,
|
||||
HeaderValue::from_str(format!("{uri_path}/").as_str())
|
||||
.with_context(|| "insert header failed")?,
|
||||
);
|
||||
response = response.status(StatusCode::MOVED_PERMANENTLY);
|
||||
let response = response
|
||||
.body(body)
|
||||
.with_context(|| "Failed to build HTTP response with body")?;
|
||||
return Ok(response);
|
||||
}
|
||||
// 生成自动目录索引
|
||||
if host_route.auto_index {
|
||||
// HTML 中的标题路径,需要移除掉配置文件中的 root = "./html" 字段
|
||||
@ -267,16 +299,8 @@ async fn stream_file(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
let null = PathBuf::from("NUL");
|
||||
#[cfg(not(windows))]
|
||||
let null = PathBuf::from("/dev/null");
|
||||
|
||||
let stream = if not_modified {
|
||||
let empty = File::open(null)
|
||||
.await
|
||||
.with_context(|| "open /dev/null failed")?;
|
||||
ReaderStream::new(empty)
|
||||
empty_stream().await?
|
||||
} else {
|
||||
ReaderStream::new(file)
|
||||
};
|
||||
@ -598,7 +622,11 @@ async fn list_dir(host_root_str: &str, path: &PathBuf) -> anyhow::Result<Vec<Dir
|
||||
.strip_prefix(&host_root_str)
|
||||
.ok_or(anyhow!("strip prefix failed"))?
|
||||
.to_string();
|
||||
let path = format!("./{path}");
|
||||
let path = if is_dir {
|
||||
format!("./{path}/")
|
||||
} else {
|
||||
format!("./{path}")
|
||||
};
|
||||
// 创建并返回目录条目信息
|
||||
let dir = DirList {
|
||||
name,
|
||||
@ -619,3 +647,24 @@ async fn list_dir(host_root_str: &str, path: &PathBuf) -> anyhow::Result<Vec<Dir
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// 创建一个空数据流,用于返回空响应或占位数据
|
||||
///
|
||||
/// 在不同操作系统上,会自动选择对应的空设备文件:
|
||||
/// - Windows: NUL
|
||||
/// - Unix/Linux: /dev/null
|
||||
///
|
||||
/// 返回一个异步流,内容为一个空文件的数据流
|
||||
///
|
||||
/// # 错误处理
|
||||
/// 如果无法打开空设备文件,会返回带有上下文信息的错误
|
||||
pub async fn empty_stream() -> anyhow::Result<ReaderStream<File>> {
|
||||
#[cfg(windows)]
|
||||
let null = PathBuf::from("NUL");
|
||||
#[cfg(not(windows))]
|
||||
let null = PathBuf::from("/dev/null");
|
||||
let empty = File::open(null)
|
||||
.await
|
||||
.with_context(|| "open /dev/null failed")?;
|
||||
Ok(ReaderStream::new(empty))
|
||||
}
|
||||
|
@ -71,7 +71,6 @@ pub async fn add_headers(Host(host): Host, req: Request, next: Next) -> impl Int
|
||||
debug!("port {:?}", port);
|
||||
let mut res = next.run(req).await;
|
||||
let req_headers = res.headers_mut();
|
||||
// let host = HOSTS.read().await;
|
||||
let Some(host) = HOSTS.get(&port) else {
|
||||
return res;
|
||||
};
|
||||
|
Reference in New Issue
Block a user