mirror of
https://github.com/DefectingCat/candy
synced 2025-07-16 09:11:36 +00:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
0879dac0ca | |||
c0a4fbfc19 | |||
b38de3a325 | |||
01bd1128c4 | |||
722bcb1c15 | |||
e9fafb3b60 | |||
5f23d5e23f | |||
53139b7ee7 | |||
254948e496 | |||
675e347c1d | |||
0671c84ae4 | |||
ba2b9539ba | |||
8288d16699 | |||
050a88b0bd | |||
0f8e34b39f | |||
96c0a3e07b | |||
0bd354983a | |||
12a4b046cf | |||
269bec83f7 | |||
8a27df2e86 | |||
38d991c393 | |||
b80bb741bc | |||
0626a4114a | |||
d3892fe3b3 | |||
cade8d3162 | |||
f98e48c3e8 | |||
7a673f9d63 | |||
46094d8e9c | |||
120657f915 | |||
0e94da7b31 | |||
487ada0e4f | |||
3f1adcc6f7 | |||
1798d1c710 | |||
3d1d86fd2e | |||
b072a63d52 | |||
ef33783dd7 | |||
3df4b6a087 | |||
57f83a4a74 | |||
d9a3562b74 | |||
9bcdb43dd5 | |||
9906bec04a | |||
d2babab848 | |||
b77645e1d3 | |||
2f1704d1d8 | |||
68ffc0337e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
logs
|
||||
.DS_Store
|
||||
config.toml
|
||||
!/.cargo/config.toml
|
||||
/html
|
||||
heaptrack.*.zst
|
||||
|
||||
|
@ -13,7 +13,8 @@ variables:
|
||||
image: "rust:latest"
|
||||
|
||||
before_script:
|
||||
- apt update -y
|
||||
- apt-get update -y
|
||||
- apt-get install -y docker.io
|
||||
- mkdir $HOME/.cargo
|
||||
- echo "[source.crates-io]" >> $HOME/.cargo/config
|
||||
- echo "replace-with = 'ustc'" >> $HOME/.cargo/config
|
||||
@ -30,13 +31,15 @@ test:
|
||||
|
||||
build:freebsd:
|
||||
stage: build
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get update -y
|
||||
- apt-get install -y musl-tools libssl-dev docker.io
|
||||
- rustup target add x86_64-unknown-freebsd
|
||||
- cross build --release --target x86_64-unknown-freebsd
|
||||
- "mv target/release/$APP_NAME target/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
- "mv target/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG public/"
|
||||
- make build-freebsd
|
||||
- "mv target/x86_64-unknown-freebsd/release/$APP_NAME target/x86_64-unknown-freebsd/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
- "mv target/x86_64-unknown-freebsd/release/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- "public/$APP_NAME-x86_64-unknown-freebsd-$CI_COMMIT_TAG"
|
||||
|
||||
build:linux-gnu-amd64:
|
||||
stage: build
|
||||
@ -55,10 +58,7 @@ build:linux-musl-amd64:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get update -y
|
||||
- apt-get install -y musl-tools libssl-dev
|
||||
- rustup target add x86_64-unknown-linux-musl
|
||||
- cargo build --release --target x86_64-unknown-linux-musl
|
||||
- make build-linux-musl
|
||||
- "mv target/x86_64-unknown-linux-musl/release/$APP_NAME target/x86_64-unknown-linux-musl/release/$APP_NAME-x86_64-unknown-linux-musl-$CI_COMMIT_TAG"
|
||||
- "mv target/x86_64-unknown-linux-musl/release/$APP_NAME-x86_64-unknown-linux-musl-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
@ -70,16 +70,25 @@ build:windows-amd64:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- apt-get install -y g++-mingw-w64-x86-64
|
||||
- rustup target add x86_64-pc-windows-gnu
|
||||
- rustup toolchain install stable-x86_64-pc-windows-gnu
|
||||
- cargo build --release --target x86_64-pc-windows-gnu
|
||||
- make build-windows-gnu
|
||||
- "mv target/x86_64-pc-windows-gnu/release/$APP_NAME.exe target/x86_64-pc-windows-gnu/release/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe"
|
||||
- "mv target/x86_64-pc-windows-gnu/release/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- public/$APP_NAME-x86_64-pc-windows-gnu-$CI_COMMIT_TAG.exe
|
||||
|
||||
build:linux-loongarch:
|
||||
stage: build
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- make build-loongarch
|
||||
- "mv target/loongarch64-unknown-linux-gnu/release/$APP_NAME target/loongarch64-unknown-linux-gnu/release/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG"
|
||||
- "mv target/loongarch64-unknown-linux-gnu/release/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG public/"
|
||||
artifacts:
|
||||
paths:
|
||||
- public/$APP_NAME-loongarch64-unknown-linux-gnu-$CI_COMMIT_TAG
|
||||
|
||||
rustdoc:
|
||||
stage: build
|
||||
rules:
|
||||
|
977
Cargo.lock
generated
977
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
@ -1,38 +1,76 @@
|
||||
cargo-features = ["profile-rustflags", "trim-paths"]
|
||||
|
||||
[package]
|
||||
name = "candy"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[features]
|
||||
ring = ["rustls/ring", "tokio-rustls/ring", "hyper-rustls/ring"]
|
||||
aws-lc-rs = [
|
||||
"rustls/aws-lc-rs",
|
||||
"tokio-rustls/aws-lc-rs",
|
||||
"hyper-rustls/aws-lc-rs",
|
||||
]
|
||||
default = ["ring"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# core
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
tokio-util = "0.7.11"
|
||||
hyper = { version = "1.4.0", features = ["full"] }
|
||||
hyper-util = { version = "0.1.6", features = ["full"] }
|
||||
http = "1.1.0"
|
||||
http-body-util = "0.1.2"
|
||||
futures-util = "0.3.30"
|
||||
async-compression = { version = "0.4.11", features = [
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
tokio-util = "0.7.14"
|
||||
hyper = { version = "1.6.0", features = ["full"] }
|
||||
hyper-util = { version = "0.1.10", features = ["full"] }
|
||||
http = "1.3.1"
|
||||
http-body-util = "0.1.3"
|
||||
futures-util = "0.3.31"
|
||||
async-compression = { version = "0.4.22", features = [
|
||||
"tokio",
|
||||
"zstd",
|
||||
"gzip",
|
||||
"deflate",
|
||||
"brotli",
|
||||
] }
|
||||
mimalloc = "0.1.44"
|
||||
# ssl
|
||||
rustls = { version = "0.23.25", default-features = false, features = [
|
||||
"logging",
|
||||
"std",
|
||||
"tls12",
|
||||
] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = [
|
||||
"logging",
|
||||
"tls12",
|
||||
] }
|
||||
hyper-rustls = { version = "0.27.5", default-features = false, features = [
|
||||
"logging",
|
||||
"tls12",
|
||||
"native-tokio",
|
||||
"http1",
|
||||
] }
|
||||
# tools
|
||||
thiserror = "1.0.61"
|
||||
anyhow = "1.0.86"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
toml = "0.8.14"
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.97"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.20"
|
||||
bytes = "1.10.1"
|
||||
# logging
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
clap = { version = "4.5.8", features = ["derive"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
clap = { version = "4.5.33", features = ["derive"] }
|
||||
rustls-pemfile = "2.2.0"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
rustflags = ["-Zthreads=8"] # Better compile performance.
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
strip = true # Remove debug symbols
|
||||
codegen-units = 1 # Allows LLVM to perform better optimization.
|
||||
lto = true # Enables link-time-optimizations.
|
||||
opt-level = 3 # Prioritizes small binary size. Use `3` if you prefer speed.
|
||||
panic = "abort" # Higher performance by disabling panic handlers.
|
||||
strip = true # Ensures debug symbols are removed.
|
||||
trim-paths = "all" # Removes potentially privileged information from your binaries.
|
||||
rustflags = ["-Cdebuginfo=0", "-C", "target-cpu=native", "-Z", "threads=8"]
|
||||
|
40
Makefile
40
Makefile
@ -1,33 +1,24 @@
|
||||
# Define the Rust compiler and cargo command
|
||||
CARGO = cargo
|
||||
RUSTC = rustc
|
||||
CROSS = cross
|
||||
CROSS = CROSS_REMOTE=1 cross
|
||||
|
||||
# Targets
|
||||
# TARGET = candy
|
||||
|
||||
# Default target to build the project
|
||||
all: build
|
||||
|
||||
# Build the project
|
||||
build:
|
||||
$(CARGO) build
|
||||
|
||||
build-release: clean
|
||||
release:
|
||||
$(CARGO) build --release
|
||||
|
||||
dev:
|
||||
CANDY_LOG=debug $(CARGO) watch -x run
|
||||
|
||||
# Run the project
|
||||
run:
|
||||
$(CARGO) run
|
||||
|
||||
# Test the project
|
||||
test:
|
||||
$(CARGO) test
|
||||
|
||||
# Clean the project
|
||||
clean:
|
||||
$(CARGO) clean
|
||||
|
||||
@ -35,35 +26,40 @@ clean-release:
|
||||
rm -rf ./target/release/
|
||||
rm -rf ./target/debug/
|
||||
|
||||
# Check the code for warnings and errors
|
||||
check:
|
||||
$(CARGO) check
|
||||
|
||||
# Format the code using rustfmt
|
||||
format:
|
||||
$(CARGO) fmt
|
||||
|
||||
# Clippy for linting
|
||||
lint:
|
||||
$(CARGO) clippy
|
||||
|
||||
fix:
|
||||
$(CARGO) fix --allow-dirty --all-features
|
||||
$(CARGO) fix --allow-dirty --all-features && $(CARGO) fmt
|
||||
|
||||
build-linux-musl: clean-release
|
||||
linux-musl: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
build-linux-gnu: clean-release
|
||||
aarch64-linux-musl: clean-release
|
||||
$(CROSS) build --release --target aarch64-unknown-linux-musl
|
||||
|
||||
aarch64-android: clean-release
|
||||
$(CROSS) build --release --target aarch64-linux-android
|
||||
|
||||
linux-gnu: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
build-windows-gnu: clean-release
|
||||
aarch64-linux-gnu: clean-release
|
||||
$(CROSS) build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
windows-gnu: clean-release
|
||||
$(CROSS) build --release --target x86_64-pc-windows-gnu
|
||||
|
||||
build-freebsd: clean-release
|
||||
freebsd: clean-release
|
||||
$(CROSS) build --release --target x86_64-unknown-freebsd
|
||||
|
||||
build-loongarch: clean-release
|
||||
loongarch: clean-release
|
||||
$(CROSS) build --release --target loongarch64-unknown-linux-gnu
|
||||
|
||||
# Phony targets to avoid conflicts with file names
|
||||
.PHONY: all build dev run test clean check format lint fix build-linux-musl build-linux-gnu build-windows-gnu build-freebsd build-loongarch
|
||||
.PHONY: all
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
A tiny web server built with rust.
|
||||
|
||||
[](https://deps.rs/repo/github/DefectingCat/candy)
|
||||

|
||||

|
||||
|
||||
|
77
TODO.md
77
TODO.md
@ -1,44 +1,49 @@
|
||||
## TODO
|
||||
|
||||
- [ ] Dockerization
|
||||
- [ ] Docs
|
||||
- [ ] Refactor HTTP 1
|
||||
- [ ] Build with compile info
|
||||
- [x] Graceful shutdown
|
||||
- [x] `keep-alive` timeout setting
|
||||
- [x] HTTP Etag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value
|
||||
- [x] Content compress
|
||||
- [x] zstd
|
||||
- [x] gzip
|
||||
- [x] deflate
|
||||
- [x] br
|
||||
- [ ] Dockerization
|
||||
- [ ] Docs
|
||||
- [x] Build with compile info
|
||||
- [x] Refactor HTTP 1
|
||||
- [x] Graceful shutdown
|
||||
- [x] `keep-alive` timeout setting
|
||||
- [x] HTTP Etag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value
|
||||
- [x] Content compress
|
||||
- [x] zstd
|
||||
- [x] gzip
|
||||
- [x] deflate
|
||||
- [x] br
|
||||
|
||||
### Configuration
|
||||
|
||||
- [x] File MIME type
|
||||
- [x] Overwrite headers in config
|
||||
- [x] Config init tests
|
||||
- [x] Error page
|
||||
- [ ] Logging to file
|
||||
- [ ] Benchs
|
||||
- [ ] Max body size
|
||||
- [x] File MIME type
|
||||
- [x] Overwrite headers in config
|
||||
- [x] Config init tests
|
||||
- [x] Error page
|
||||
- [ ] Logging to file
|
||||
- [ ] Benchs
|
||||
- [ ] Max body size
|
||||
- [ ] HTTP 2
|
||||
|
||||
### Features
|
||||
|
||||
- [x] Cross platform compile
|
||||
- [x] x86_64-unknown-linux-gnu
|
||||
- [x] x86_64-unknown-linux-musl
|
||||
- [x] aarch64-unknown-linux-gnu
|
||||
- [x] aarch64-unknown-linux-musl
|
||||
- [x] x86_64-pc-windows-gnu
|
||||
- [x] x86_64-unknown-freebsd
|
||||
- [x] loongarch64-unknown-linux-gnu
|
||||
- [ ] Proxy
|
||||
- [ ] Reverse Proxy
|
||||
- [ ] FastCGI
|
||||
- [ ] SSL
|
||||
- [x] Cli
|
||||
- [x] Specific custom config location
|
||||
- [ ] HTTP 2
|
||||
- [ ] HTTP 3
|
||||
- [ ] Specify thread numbers
|
||||
- [x] Cross platform compile
|
||||
- [x] x86_64-unknown-linux-gnu
|
||||
- [x] x86_64-unknown-linux-musl
|
||||
- [x] aarch64-unknown-linux-gnu
|
||||
- [x] aarch64-unknown-linux-musl
|
||||
- [x] x86_64-pc-windows-gnu
|
||||
- [x] x86_64-unknown-freebsd
|
||||
- [x] loongarch64-unknown-linux-gnu
|
||||
- [ ] Load balance
|
||||
- [ ] Pingora
|
||||
- [ ] Proxy
|
||||
- [x] Reverse Proxy
|
||||
- [x] Connect to upstream timeout setting
|
||||
- [x] Follow http 301
|
||||
- [x] Custom headers
|
||||
- [x] SSL
|
||||
- [x] Cli
|
||||
- [x] Specific custom config location
|
||||
- [x] HTTP 2
|
||||
- [ ] HTTP 3
|
||||
- [ ] Specify thread numbers
|
||||
|
33
build.rs
33
build.rs
@ -17,9 +17,10 @@ macro_rules! set_env {
|
||||
|
||||
fn main() {
|
||||
rustc_info();
|
||||
println!("cargo:info=test");
|
||||
commit_info();
|
||||
}
|
||||
|
||||
/// Get rustc version info
|
||||
fn rustc_info() {
|
||||
let rustc_output = Command::new("rustc")
|
||||
.args(["-vV"])
|
||||
@ -31,6 +32,34 @@ fn rustc_info() {
|
||||
.split('\n')
|
||||
.filter(|info| !info.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
set_env!("RUA_COMPILER={}", info_arr[0]);
|
||||
}
|
||||
|
||||
/// Get commit info
|
||||
/// If failed, set RUA_COMMIT=unknown
|
||||
fn commit_info() {
|
||||
let output = Command::new("git").args(["rev-parse", "HEAD"]).output();
|
||||
// get commit info
|
||||
let Ok(output) = output else {
|
||||
warn!("get commit info failed");
|
||||
set_env!("RUA_COMMIT={}", "unknown");
|
||||
return;
|
||||
};
|
||||
// check is dirty
|
||||
let dirty = Command::new("git")
|
||||
.args(["diff", "--quiet"])
|
||||
.output()
|
||||
.is_ok();
|
||||
let commit = String::from_utf8_lossy(&output.stdout)
|
||||
.chars()
|
||||
.take(7)
|
||||
.collect::<String>();
|
||||
let commit = if commit.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else if dirty {
|
||||
format!("{commit}-dirty")
|
||||
} else {
|
||||
commit
|
||||
};
|
||||
set_env!("RUA_COMMIT={}", commit);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 4000
|
||||
|
||||
[[host.route]]
|
||||
# route path: GET /
|
||||
location = "/"
|
||||
|
@ -1,17 +1,30 @@
|
||||
# Default file type for unknow file
|
||||
default_type = "application/octet-stream"
|
||||
# Custom MIME types
|
||||
[types]
|
||||
wasm = "application/wasm"
|
||||
|
||||
# Virtual host
|
||||
[[host]]
|
||||
ip = "0.0.0.0"
|
||||
port = 4000
|
||||
# Connection timeout
|
||||
timeout = 15
|
||||
# SSL certificate
|
||||
# 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"
|
||||
|
||||
# Add custom headers to response
|
||||
[host.headers]
|
||||
X-Powered-By = "candy"
|
||||
|
||||
# Routes for virtual host
|
||||
[[host.route]]
|
||||
# If has index field, it will be used as static file host
|
||||
index = ["index.html"]
|
||||
# Route location
|
||||
location = "/"
|
||||
root = "./html"
|
||||
[host.route.error_page]
|
||||
@ -27,6 +40,8 @@ status = 404
|
||||
page = "404.html"
|
||||
|
||||
[[host.route]]
|
||||
index = ["index.html"]
|
||||
location = "/proxy/"
|
||||
# If has root field, it will be used as reverse proxy
|
||||
proxy_pass = "http://localhost:3000/"
|
||||
# Timeout for connect to upstream
|
||||
proxy_timeout = 10
|
||||
|
@ -7,3 +7,6 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
1
docs/.nvmrc
Normal file
1
docs/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
lts/*
|
3
docs/.prettierignore
Normal file
3
docs/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
.docusaurus/
|
||||
build/
|
||||
node_modules/
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
PACKAGE_MANAGER = pnpm
|
||||
|
||||
all: pack
|
||||
|
||||
nvm:
|
||||
fnm use && $(CMD)
|
||||
|
||||
deps:
|
||||
make nvm CMD="$(PACKAGE_MANAGER) i"
|
||||
|
||||
fix:
|
||||
make nvm CMD="$(PACKAGE_MANAGER) i && $(PACKAGE_MANAGER) pretty"
|
||||
|
||||
pack:
|
||||
cp -r docs/* i18n/zh-Hans/docusaurus-plugin-content-docs/current \
|
||||
&& make nvm CMD="$(PACKAGE_MANAGER) i && $(PACKAGE_MANAGER) build"
|
||||
|
||||
# build:
|
||||
|
||||
.PHONY: all
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
@ -6,3 +6,5 @@ tags: [hello]
|
||||
---
|
||||
|
||||
## Hello World
|
||||
|
||||
<!-- truncate -->
|
||||
|
4
docs/docs/config/_category_.json
Normal file
4
docs/docs/config/_category_.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
9
docs/docs/config/config-file.md
Normal file
9
docs/docs/config/config-file.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: 配置文件
|
||||
sidebar_position: 1
|
||||
title: 配置文件
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
@ -2,4 +2,24 @@
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Introduction
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
||||
|
||||
## 安装
|
||||
|
||||
TODO
|
||||
|
||||
## 使用
|
||||
|
||||
Candy 支持单个可执行文件运行:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { themes as prismThemes } from 'prism-react-renderer';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import { themes as prismThemes } from 'prism-react-renderer';
|
||||
|
||||
const config: Config = {
|
||||
title: 'Candy',
|
||||
tagline: 'Tiny http web server',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
future: {
|
||||
experimental_faster: true,
|
||||
},
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://your-docusaurus-site.example.com',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
@ -25,7 +29,7 @@ const config: Config = {
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'zh-Hans',
|
||||
locales: ['en', 'zh-Hans'],
|
||||
},
|
||||
|
||||
|
313
docs/i18n/en/code.json
Normal file
313
docs/i18n/en/code.json
Normal file
@ -0,0 +1,313 @@
|
||||
{
|
||||
"theme.ErrorPageContent.title": {
|
||||
"message": "This page crashed.",
|
||||
"description": "The title of the fallback page when the page crashed"
|
||||
},
|
||||
"theme.BackToTopButton.buttonAriaLabel": {
|
||||
"message": "Scroll back to top",
|
||||
"description": "The ARIA label for the back to top button"
|
||||
},
|
||||
"theme.blog.archive.title": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero title of the blog archive page"
|
||||
},
|
||||
"theme.blog.archive.description": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero description of the blog archive page"
|
||||
},
|
||||
"theme.blog.paginator.navAriaLabel": {
|
||||
"message": "Blog list page navigation",
|
||||
"description": "The ARIA label for the blog pagination"
|
||||
},
|
||||
"theme.blog.paginator.newerEntries": {
|
||||
"message": "Newer entries",
|
||||
"description": "The label used to navigate to the newer blog posts page (previous page)"
|
||||
},
|
||||
"theme.blog.paginator.olderEntries": {
|
||||
"message": "Older entries",
|
||||
"description": "The label used to navigate to the older blog posts page (next page)"
|
||||
},
|
||||
"theme.blog.post.paginator.navAriaLabel": {
|
||||
"message": "Blog post page navigation",
|
||||
"description": "The ARIA label for the blog posts pagination"
|
||||
},
|
||||
"theme.blog.post.paginator.newerPost": {
|
||||
"message": "Newer post",
|
||||
"description": "The blog post button label to navigate to the newer/previous post"
|
||||
},
|
||||
"theme.blog.post.paginator.olderPost": {
|
||||
"message": "Older post",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "View all tags",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "Switch between dark and light mode (currently {mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "dark mode",
|
||||
"description": "The name for the dark color mode"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.light": {
|
||||
"message": "light mode",
|
||||
"description": "The name for the light color mode"
|
||||
},
|
||||
"theme.docs.breadcrumbs.navAriaLabel": {
|
||||
"message": "Breadcrumbs",
|
||||
"description": "The ARIA label for the breadcrumbs"
|
||||
},
|
||||
"theme.docs.DocCard.categoryDescription.plurals": {
|
||||
"message": "1 item|{count} items",
|
||||
"description": "The default description for a category card in the generated index about how many items this category includes"
|
||||
},
|
||||
"theme.docs.paginator.navAriaLabel": {
|
||||
"message": "Docs pages",
|
||||
"description": "The ARIA label for the docs pagination"
|
||||
},
|
||||
"theme.docs.paginator.previous": {
|
||||
"message": "Previous",
|
||||
"description": "The label used to navigate to the previous doc"
|
||||
},
|
||||
"theme.docs.paginator.next": {
|
||||
"message": "Next",
|
||||
"description": "The label used to navigate to the next doc"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle.nDocsTagged": {
|
||||
"message": "One doc tagged|{count} docs tagged",
|
||||
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle": {
|
||||
"message": "{nDocsTagged} with \"{tagName}\"",
|
||||
"description": "The title of the page for a docs tag"
|
||||
},
|
||||
"theme.docs.versions.unreleasedVersionLabel": {
|
||||
"message": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"description": "The label used to tell the user that he's browsing an unreleased doc version"
|
||||
},
|
||||
"theme.docs.versions.unmaintainedVersionLabel": {
|
||||
"message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": {
|
||||
"message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"description": "The label used to tell the user to check the latest version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionLinkLabel": {
|
||||
"message": "latest version",
|
||||
"description": "The label used for the latest version suggestion link label"
|
||||
},
|
||||
"theme.docs.versionBadge.label": {
|
||||
"message": "Version: {versionLabel}"
|
||||
},
|
||||
"theme.common.editThisPage": {
|
||||
"message": "Edit this page",
|
||||
"description": "The link label to edit the current page"
|
||||
},
|
||||
"theme.common.headingLinkTitle": {
|
||||
"message": "Direct link to {heading}",
|
||||
"description": "Title for link to heading"
|
||||
},
|
||||
"theme.NotFound.title": {
|
||||
"message": "Page Not Found",
|
||||
"description": "The title of the 404 page"
|
||||
},
|
||||
"theme.navbar.mobileVersionsDropdown.label": {
|
||||
"message": "Versions",
|
||||
"description": "The label for the navbar versions dropdown on mobile view"
|
||||
},
|
||||
"theme.lastUpdated.atDate": {
|
||||
"message": " on {date}",
|
||||
"description": "The words used to describe on which date a page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.byUser": {
|
||||
"message": " by {user}",
|
||||
"description": "The words used to describe by who the page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.lastUpdatedAtBy": {
|
||||
"message": "Last updated{atDate}{byUser}",
|
||||
"description": "The sentence used to display when a page has been last updated, and by who"
|
||||
},
|
||||
"theme.tags.tagsListLabel": {
|
||||
"message": "Tags:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "Close",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "caution",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
},
|
||||
"theme.admonition.danger": {
|
||||
"message": "danger",
|
||||
"description": "The default label used for the Danger admonition (:::danger)"
|
||||
},
|
||||
"theme.admonition.info": {
|
||||
"message": "info",
|
||||
"description": "The default label used for the Info admonition (:::info)"
|
||||
},
|
||||
"theme.admonition.note": {
|
||||
"message": "note",
|
||||
"description": "The default label used for the Note admonition (:::note)"
|
||||
},
|
||||
"theme.admonition.tip": {
|
||||
"message": "tip",
|
||||
"description": "The default label used for the Tip admonition (:::tip)"
|
||||
},
|
||||
"theme.admonition.warning": {
|
||||
"message": "warning",
|
||||
"description": "The default label used for the Warning admonition (:::warning)"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "Blog recent posts navigation",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "Copied",
|
||||
"description": "The copied button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.copyButtonAriaLabel": {
|
||||
"message": "Copy code to clipboard",
|
||||
"description": "The ARIA label for copy code blocks button"
|
||||
},
|
||||
"theme.CodeBlock.copy": {
|
||||
"message": "Copy",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "Toggle word wrap",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.DocSidebarItem.expandCategoryAriaLabel": {
|
||||
"message": "Expand sidebar category '{label}'",
|
||||
"description": "The ARIA label to expand the sidebar category"
|
||||
},
|
||||
"theme.DocSidebarItem.collapseCategoryAriaLabel": {
|
||||
"message": "Collapse sidebar category '{label}'",
|
||||
"description": "The ARIA label to collapse the sidebar category"
|
||||
},
|
||||
"theme.NavBar.navAriaLabel": {
|
||||
"message": "Main",
|
||||
"description": "The ARIA label for the main navigation"
|
||||
},
|
||||
"theme.NotFound.p1": {
|
||||
"message": "We could not find what you were looking for.",
|
||||
"description": "The first paragraph of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p2": {
|
||||
"message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
|
||||
"description": "The 2nd paragraph of the 404 page"
|
||||
},
|
||||
"theme.navbar.mobileLanguageDropdown.label": {
|
||||
"message": "Languages",
|
||||
"description": "The label for the mobile language switcher dropdown"
|
||||
},
|
||||
"theme.TOCCollapsible.toggleButtonLabel": {
|
||||
"message": "On this page",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "Read more",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
},
|
||||
"theme.blog.post.readMoreLabel": {
|
||||
"message": "Read more about {title}",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "One min read|{readingTime} min read",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "Home page",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonTitle": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.navAriaLabel": {
|
||||
"message": "Docs sidebar",
|
||||
"description": "The ARIA label for the sidebar navigation"
|
||||
},
|
||||
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
|
||||
"message": "Close navigation bar",
|
||||
"description": "The ARIA label for close button of mobile sidebar"
|
||||
},
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
|
||||
"message": "← Back to main menu",
|
||||
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
|
||||
},
|
||||
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
|
||||
"message": "Toggle navigation bar",
|
||||
"description": "The ARIA label for hamburger menu button of mobile navigation"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonTitle": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "One post|{count} posts",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} tagged with \"{tagName}\"",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.blog.author.pageTitle": {
|
||||
"message": "{authorName} - {nPosts}",
|
||||
"description": "The title of the page for a blog author"
|
||||
},
|
||||
"theme.blog.authorsList.pageTitle": {
|
||||
"message": "Authors",
|
||||
"description": "The title of the authors page"
|
||||
},
|
||||
"theme.blog.authorsList.viewAll": {
|
||||
"message": "View all authors",
|
||||
"description": "The label of the link targeting the blog authors page"
|
||||
},
|
||||
"theme.blog.author.noPosts": {
|
||||
"message": "This author has not written any posts yet.",
|
||||
"description": "The text for authors with 0 blog post"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.title": {
|
||||
"message": "Unlisted page",
|
||||
"description": "The unlisted content banner title"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.message": {
|
||||
"message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.title": {
|
||||
"message": "Draft page",
|
||||
"description": "The draft content banner title"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.message": {
|
||||
"message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||
"description": "The draft content banner message"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "Try again",
|
||||
"description": "The label of the button to try again rendering when the React error boundary captures an error"
|
||||
},
|
||||
"theme.common.skipToMainContent": {
|
||||
"message": "Skip to main content",
|
||||
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
|
||||
},
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "Tags",
|
||||
"description": "The title of the tag list page"
|
||||
}
|
||||
}
|
14
docs/i18n/en/docusaurus-plugin-content-blog/options.json
Normal file
14
docs/i18n/en/docusaurus-plugin-content-blog/options.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "Blog",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "Blog",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "Recent posts",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
10
docs/i18n/en/docusaurus-plugin-content-docs/current.json
Normal file
10
docs/i18n/en/docusaurus-plugin-content-docs/current.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "Next",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.tutorialSidebar.category.配置": {
|
||||
"message": "配置",
|
||||
"description": "The label for category 配置 in sidebar tutorialSidebar"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: Config File
|
||||
sidebar_position: 1
|
||||
title: Config File
|
||||
---
|
||||
|
||||
## Config File
|
||||
|
||||
Candy follows the config file to configure.
|
25
docs/i18n/en/docusaurus-plugin-content-docs/current/intro.md
Normal file
25
docs/i18n/en/docusaurus-plugin-content-docs/current/intro.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Candy is a lightweight HTTP server, aiming to quickly deploy a high-performance HTTP server.
|
||||
|
||||
## Installation
|
||||
|
||||
TODO
|
||||
|
||||
## Usage
|
||||
|
||||
Candy supports single executable file running:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
6
docs/i18n/en/docusaurus-theme-classic/footer.json
Normal file
6
docs/i18n/en/docusaurus-theme-classic/footer.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "Copyright © 2024 Candy, Inc. Built with Docusaurus.",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
22
docs/i18n/en/docusaurus-theme-classic/navbar.json
Normal file
22
docs/i18n/en/docusaurus-theme-classic/navbar.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "RUA",
|
||||
"description": "The title in the navbar"
|
||||
},
|
||||
"logo.alt": {
|
||||
"message": "My Site Logo",
|
||||
"description": "The alt text of navbar logo"
|
||||
},
|
||||
"item.label.Tutorial": {
|
||||
"message": "Tutorial",
|
||||
"description": "Navbar item with label Tutorial"
|
||||
},
|
||||
"item.label.Blog": {
|
||||
"message": "Blog",
|
||||
"description": "Navbar item with label Blog"
|
||||
},
|
||||
"item.label.GitHub": {
|
||||
"message": "GitHub",
|
||||
"description": "Navbar item with label GitHub"
|
||||
}
|
||||
}
|
@ -39,14 +39,6 @@
|
||||
"message": "较旧一篇",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "{count} 篇博文",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} 含有标签「{tagName}」",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "查看所有标签",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
@ -142,6 +134,10 @@
|
||||
"message": "标签:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "关闭",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "警告",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
@ -166,14 +162,14 @@
|
||||
"message": "注意",
|
||||
"description": "The default label used for the Warning admonition (:::warning)"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "关闭",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "最近博文导航",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "切换自动换行",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "复制成功",
|
||||
"description": "The copied button label on code blocks"
|
||||
@ -186,10 +182,6 @@
|
||||
"message": "复制",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "切换自动换行",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.DocSidebarItem.expandCategoryAriaLabel": {
|
||||
"message": "展开侧边栏分类 '{label}'",
|
||||
"description": "The ARIA label to expand the sidebar category"
|
||||
@ -218,6 +210,10 @@
|
||||
"message": "本页总览",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "阅读需 {readingTime} 分钟",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "阅读更多",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
@ -226,10 +222,6 @@
|
||||
"message": "阅读 {title} 的全文",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "阅读需 {readingTime} 分钟",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "主页面",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
@ -266,6 +258,46 @@
|
||||
"message": "展开侧边栏",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "{count} 篇博文",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} 含有标签「{tagName}」",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.blog.author.pageTitle": {
|
||||
"message": "{authorName} - {nPosts}",
|
||||
"description": "The title of the page for a blog author"
|
||||
},
|
||||
"theme.blog.authorsList.pageTitle": {
|
||||
"message": "Authors",
|
||||
"description": "The title of the authors page"
|
||||
},
|
||||
"theme.blog.authorsList.viewAll": {
|
||||
"message": "View All Authors",
|
||||
"description": "The label of the link targeting the blog authors page"
|
||||
},
|
||||
"theme.blog.author.noPosts": {
|
||||
"message": "This author has not written any posts yet.",
|
||||
"description": "The text for authors with 0 blog post"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.title": {
|
||||
"message": "未列出页",
|
||||
"description": "The unlisted content banner title"
|
||||
},
|
||||
"theme.contentVisibility.unlistedBanner.message": {
|
||||
"message": "此页面未列出。搜索引擎不会对其索引,只有拥有直接链接的用户才能访问。",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.title": {
|
||||
"message": "Draft page",
|
||||
"description": "The draft content banner title"
|
||||
},
|
||||
"theme.contentVisibility.draftBanner.message": {
|
||||
"message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||
"description": "The draft content banner message"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "重试",
|
||||
"description": "The label of the button to try again rendering when the React error boundary captures an error"
|
||||
@ -277,13 +309,5 @@
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "标签",
|
||||
"description": "The title of the tag list page"
|
||||
},
|
||||
"theme.unlistedContent.title": {
|
||||
"message": "未列出页",
|
||||
"description": "The unlisted content banner title"
|
||||
},
|
||||
"theme.unlistedContent.message": {
|
||||
"message": "此页面未列出。搜索引擎不会对其索引,只有拥有直接链接的用户才能访问。",
|
||||
"description": "The unlisted content banner message"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
slug: hello-world
|
||||
title: Hello World
|
||||
authors: xfy
|
||||
tags: [hello]
|
||||
---
|
||||
|
||||
## 你好,世界
|
@ -1,5 +0,0 @@
|
||||
xfy:
|
||||
name: xfy
|
||||
title: xfy
|
||||
url: https://github.com/DefectingCat
|
||||
image_url: https://avatars.githubusercontent.com/u/25033493?v=4
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "博客",
|
||||
"message": "Blog",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "博客",
|
||||
"message": "Blog",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "近期文章",
|
||||
"message": "Recent posts",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
## 配置文件
|
@ -2,5 +2,9 @@
|
||||
"version.label": {
|
||||
"message": "Next",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.tutorialSidebar.category.配置": {
|
||||
"message": "配置",
|
||||
"description": "The label for category 配置 in sidebar tutorialSidebar"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "配置",
|
||||
"position": 2
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebar_label: 配置文件
|
||||
sidebar_position: 1
|
||||
title: 配置文件
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
Candy 遵循配置文件进行配置。
|
@ -3,3 +3,23 @@ sidebar_position: 1
|
||||
---
|
||||
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
||||
|
||||
## 安装
|
||||
|
||||
TODO
|
||||
|
||||
## 使用
|
||||
|
||||
Candy 支持单个可执行文件运行:
|
||||
|
||||
```bash
|
||||
❯ ./target/release/candy -h
|
||||
Usage: candy [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <FILE> Sets a custom config file [default: ./config.toml]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# 介绍
|
||||
|
||||
Candy 是一个轻量级的 HTTP 服务器,旨在快速部署一个高性能的 HTTP 服务器。
|
@ -4,7 +4,7 @@
|
||||
"description": "The title in the navbar"
|
||||
},
|
||||
"logo.alt": {
|
||||
"message": "My Site Logo",
|
||||
"message": "Candy logo",
|
||||
"description": "The alt text of navbar logo"
|
||||
},
|
||||
"item.label.Tutorial": {
|
||||
@ -12,7 +12,7 @@
|
||||
"description": "Navbar item with label Tutorial"
|
||||
},
|
||||
"item.label.Blog": {
|
||||
"message": "博客",
|
||||
"message": "Blog",
|
||||
"description": "Navbar item with label Blog"
|
||||
},
|
||||
"item.label.GitHub": {
|
||||
|
@ -6,30 +6,35 @@
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start-zh": "docusaurus start --locale zh-Hans",
|
||||
"build": "docusaurus build",
|
||||
"build": "docusaurus write-translations --locale en && docusaurus write-translations --locale zh-Hans && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-translations-zh": "docusaurus write-translations --locale zh-Hans",
|
||||
"write-translations": "docusaurus write-translations --locale zh-Hans",
|
||||
"write-translations-en": "docusaurus write-translations --locale en",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
"pretty": "prettier --cache --write \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown --plugin=prettier-plugin-organize-imports",
|
||||
"pretty-check": "prettier --cache \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown --check --plugin=prettier-plugin-organize-imports",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.3.2",
|
||||
"@docusaurus/preset-classic": "3.3.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"@docusaurus/core": "3.6.3",
|
||||
"@docusaurus/faster": "^3.6.3",
|
||||
"@docusaurus/preset-classic": "3.6.3",
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.3.2",
|
||||
"@docusaurus/tsconfig": "3.3.2",
|
||||
"@docusaurus/types": "3.3.2",
|
||||
"typescript": "~5.2.2"
|
||||
"@docusaurus/module-type-aliases": "3.6.3",
|
||||
"@docusaurus/tsconfig": "3.6.3",
|
||||
"@docusaurus/types": "3.6.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -46,5 +51,5 @@
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.2+sha512.127dc83b9ea10c32be65d22a8efb4a65fb952e8fefbdfded39bdc3c97efc32d31b48b00420df2c1187ace28c921c902f0cb5a134a4d032b8b5295cbfa2c681e2"
|
||||
"packageManager": "pnpm@9.15.2"
|
||||
}
|
||||
|
7170
docs/pnpm-lock.yaml
generated
7170
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
||||
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
@ -12,7 +12,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
||||
*/
|
||||
const sidebars: SidebarsConfig = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
|
@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import Heading from '@theme/Heading';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type FeatureItem = {
|
||||
@ -41,7 +41,7 @@ const FeatureList: FeatureItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({title, Svg, description}: FeatureItem) {
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
|
@ -1,14 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
import Heading from '@theme/Heading';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
function HomepageHeader() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
@ -19,7 +19,8 @@ function HomepageHeader() {
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro">
|
||||
to="/docs/intro"
|
||||
>
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
@ -29,11 +30,12 @@ function HomepageHeader() {
|
||||
}
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Hello from ${siteConfig.title}`}
|
||||
description="Description will go into a meta tag in <head />">
|
||||
description="Description will go into a meta tag in <head />"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
@ -2,8 +2,9 @@ use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
/// A tiny HTTP server.
|
||||
pub struct Cli {
|
||||
/// Sets a custom config file
|
||||
/// Set a custom config file location.
|
||||
#[arg(short, long, value_name = "FILE", default_value = "./config.toml")]
|
||||
pub config: String,
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use crate::{
|
||||
consts::{host_index, insert_default_mimes, mime_default, timeout_default, types_default},
|
||||
consts::{
|
||||
host_index, insert_default_mimes, mime_default, timeout_default, types_default,
|
||||
upstream_timeout_default,
|
||||
},
|
||||
error::Result,
|
||||
};
|
||||
use std::{borrow::Cow, collections::BTreeMap, fs};
|
||||
@ -13,6 +16,8 @@ pub struct ErrorRoute {
|
||||
pub page: String,
|
||||
}
|
||||
|
||||
/// Route in virtual host
|
||||
/// Can be a static file or a reverse proxy
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct SettingRoute {
|
||||
/// The register route
|
||||
@ -22,34 +27,56 @@ pub struct SettingRoute {
|
||||
/// Index files format
|
||||
#[serde(default = "host_index")]
|
||||
pub index: Vec<String>,
|
||||
/// Custom error page
|
||||
pub error_page: Option<ErrorRoute>,
|
||||
// reverse proxy url
|
||||
|
||||
/// Reverse proxy url
|
||||
pub proxy_pass: Option<String>,
|
||||
/// Timeout for connect to upstream
|
||||
#[serde(default = "upstream_timeout_default")]
|
||||
pub proxy_timeout: u16,
|
||||
}
|
||||
|
||||
/// Host routes
|
||||
/// Each host can have multiple routes
|
||||
pub type HostRouteMap = BTreeMap<String, SettingRoute>;
|
||||
|
||||
/// Virtual host
|
||||
/// Each host can listen on one port and one ip
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct SettingHost {
|
||||
/// Host ip
|
||||
pub ip: String,
|
||||
/// Host port
|
||||
pub port: u32,
|
||||
/// SSL certificate location
|
||||
pub certificate: Option<String>,
|
||||
/// ssl key location
|
||||
pub certificate_key: Option<String>,
|
||||
route: Vec<Option<SettingRoute>>,
|
||||
/// Host route map
|
||||
#[serde(skip_deserializing, skip_serializing)]
|
||||
pub route_map: HostRouteMap,
|
||||
/// HTTP keep-alive timeout
|
||||
#[serde(default = "timeout_default")]
|
||||
pub timeout: u16,
|
||||
/// HTTP headers
|
||||
/// Used to overwrite headers in config
|
||||
pub headers: Option<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
pub type MIMEType = BTreeMap<Cow<'static, str>, Cow<'static, str>>;
|
||||
|
||||
/// Whole config settings
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Settings {
|
||||
/// Default file type for unknow file
|
||||
#[serde(default = "mime_default")]
|
||||
pub default_type: Cow<'static, str>,
|
||||
/// MIME types
|
||||
#[serde(default = "types_default")]
|
||||
pub types: MIMEType,
|
||||
/// Virtual host
|
||||
pub host: Vec<SettingHost>,
|
||||
}
|
||||
|
||||
@ -60,14 +87,12 @@ impl Settings {
|
||||
|
||||
// convert route map
|
||||
settings.host.iter_mut().for_each(|host| {
|
||||
let routes = &mut host.route;
|
||||
for route in routes.iter_mut() {
|
||||
if route.is_none() {
|
||||
continue;
|
||||
}
|
||||
let route = route.take().unwrap();
|
||||
host.route_map.insert(route.location.to_string(), route);
|
||||
}
|
||||
host.route
|
||||
.iter_mut()
|
||||
.filter_map(Option::take)
|
||||
.for_each(|route| {
|
||||
host.route_map.insert(route.location.to_string(), route);
|
||||
});
|
||||
});
|
||||
|
||||
// combine mime types
|
||||
|
@ -1,20 +1,14 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, env, process::exit, sync::OnceLock};
|
||||
use std::{borrow::Cow, collections::BTreeMap, env, sync::OnceLock};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::config::{MIMEType, Settings};
|
||||
use crate::{
|
||||
config::{MIMEType, Settings},
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
// global settings
|
||||
pub static SETTINGS: OnceLock<Settings> = OnceLock::new();
|
||||
pub fn get_settings() -> &'static Settings {
|
||||
SETTINGS.get_or_init(|| {
|
||||
Settings::new("")
|
||||
.map_err(|err| {
|
||||
error!("get_or_init config failed: {err}");
|
||||
exit(1);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
pub fn get_settings() -> Result<&'static Settings> {
|
||||
SETTINGS.get().ok_or(Error::Empty)
|
||||
}
|
||||
|
||||
// pre defined
|
||||
@ -23,6 +17,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const OS: &str = env::consts::OS;
|
||||
pub const ARCH: &str = env::consts::ARCH;
|
||||
pub const COMPILER: &str = env!("RUA_COMPILER");
|
||||
pub const COMMIT: &str = env!("RUA_COMMIT");
|
||||
|
||||
// config defaults
|
||||
pub const HOST_INDEX: [&str; 1] = ["index.html"];
|
||||
@ -42,6 +37,12 @@ pub fn mime_default() -> Cow<'static, str> {
|
||||
MIME_DEFAULT.into()
|
||||
}
|
||||
|
||||
// default reverse proxy upstream timeout
|
||||
pub const UPSTREAM_TIMEOUT: u16 = 5;
|
||||
pub fn upstream_timeout_default() -> u16 {
|
||||
UPSTREAM_TIMEOUT
|
||||
}
|
||||
|
||||
// default mime types
|
||||
pub fn types_default() -> MIMEType {
|
||||
BTreeMap::new()
|
||||
|
@ -34,6 +34,8 @@ pub enum Error {
|
||||
InvalidHeader(#[from] InvalidHeaderValue),
|
||||
#[error("")]
|
||||
Empty,
|
||||
#[error("missing header {0}")]
|
||||
MissingHeader(&'static str),
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = anyhow::Result<T, E>;
|
||||
|
101
src/http/client.rs
Normal file
101
src/http/client.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use bytes::Bytes;
|
||||
use http::{request::Parts, HeaderValue, Request, Response, Uri};
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Incoming;
|
||||
use hyper_rustls::ConfigBuilderExt;
|
||||
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
const MAX_REDIRECTS: usize = 10;
|
||||
|
||||
/// Get http response
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `url`: http url
|
||||
/// `parts`: http request parts
|
||||
/// `body`: http request body
|
||||
///
|
||||
/// ## Return
|
||||
///
|
||||
/// `anyhow::Result<Response<Incoming>>`
|
||||
pub async fn get_inner(url: Uri, parts: Parts, body: Bytes) -> anyhow::Result<Response<Incoming>> {
|
||||
// Set a process wide default crypto provider.
|
||||
#[cfg(feature = "ring")]
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
#[cfg(feature = "aws-lc-rs")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
// Prepare the TLS client config
|
||||
// Default TLS client config with native roots
|
||||
let tls = rustls::ClientConfig::builder()
|
||||
.with_native_roots()?
|
||||
.with_no_client_auth();
|
||||
|
||||
// Prepare the HTTPS connector
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_tls_config(tls)
|
||||
.https_or_http()
|
||||
.enable_http1()
|
||||
.build();
|
||||
|
||||
// Build the hyper client from the HTTPS connector.
|
||||
let client: Client<_, Full<Bytes>> = Client::builder(TokioExecutor::new()).build(https);
|
||||
let host_url = url.clone();
|
||||
let host = host_url.host().ok_or(Error::InternalServerError(anyhow!(
|
||||
"proxy pass host incorrect"
|
||||
)))?;
|
||||
let mut req: Request<Full<Bytes>> = hyper::Request::builder()
|
||||
.method(parts.method.clone())
|
||||
.uri(url)
|
||||
.body(Full::from(body))
|
||||
.with_context(|| "request builder")?;
|
||||
// Add client request headers to request, and remove host header
|
||||
req.headers_mut().extend(parts.headers);
|
||||
req.headers_mut()
|
||||
.insert("host", HeaderValue::from_str(host)?);
|
||||
|
||||
let res = client.request(req).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get http response Body
|
||||
/// And follo redirects
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `url`: http url
|
||||
/// `parts`: http request parts
|
||||
/// `body`: http request body
|
||||
///
|
||||
/// ## Return
|
||||
///
|
||||
/// `anyhow::Result<Response<Incoming>>`
|
||||
pub async fn get(url: Uri, parts: Parts, body: Bytes) -> anyhow::Result<Response<Incoming>> {
|
||||
let mut redirects = 0;
|
||||
|
||||
let mut res = get_inner(url, parts.clone(), body.clone()).await?;
|
||||
while (res.status() == 301 || res.status() == 302) && redirects < MAX_REDIRECTS {
|
||||
let (parts_inner, body_inner) = (parts.clone(), body.clone());
|
||||
redirects += 1;
|
||||
let location = res
|
||||
.headers()
|
||||
.get("location")
|
||||
.ok_or(Error::MissingHeader("location"))
|
||||
.with_context(|| "missing header location")?
|
||||
.to_str()
|
||||
.with_context(|| "failed to convert header value to str")?
|
||||
.to_string();
|
||||
let url = Uri::from_str(&location).with_context(|| "failed to convert str to url")?;
|
||||
debug!("proxy redirect to {url}");
|
||||
res = get_inner(url, parts_inner, body_inner).await?;
|
||||
}
|
||||
|
||||
debug!("get_inner response headers: {:?}", res.headers());
|
||||
Ok(res)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod client;
|
||||
pub mod mime;
|
||||
pub mod response;
|
||||
|
||||
|
@ -9,6 +9,7 @@ use crate::{
|
||||
consts::{NAME, VERSION},
|
||||
error::{Error, Result},
|
||||
get_settings,
|
||||
http::client,
|
||||
utils::{
|
||||
compress::{stream_compress, CompressType},
|
||||
find_route, parse_assets_path,
|
||||
@ -18,56 +19,54 @@ use crate::{
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures_util::TryStreamExt;
|
||||
use http::{response::Builder, Method};
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full, StreamBody};
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
|
||||
use hyper::{
|
||||
body::{Bytes, Frame, Incoming},
|
||||
Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncBufRead, BufReader},
|
||||
net::TcpStream,
|
||||
select,
|
||||
};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
/// HTTP handler
|
||||
/// Candy handler
|
||||
///
|
||||
/// The lifetime of the handler is the lifetime of the request. the request and response will be
|
||||
/// move into the handler. Not the reference.
|
||||
#[derive(Debug)]
|
||||
pub struct CandyHandler<'req> {
|
||||
/// Request from hyper
|
||||
pub req: &'req Request<Incoming>,
|
||||
pub req: Request<Incoming>,
|
||||
/// Hyper response
|
||||
pub res: Builder,
|
||||
/// Config host field
|
||||
host: &'static SettingHost,
|
||||
/// Router
|
||||
router: &'req SettingRoute,
|
||||
router: Option<&'req SettingRoute>,
|
||||
/// Current request's assets path
|
||||
assets_path: &'req str,
|
||||
assets_path: Option<&'req str>,
|
||||
}
|
||||
|
||||
pub type CandyBody<T, E = Error> = BoxBody<T, E>;
|
||||
type CandyResponse = Result<Response<CandyBody<Bytes>>>;
|
||||
impl<'req> CandyHandler<'req> {
|
||||
impl CandyHandler<'_> {
|
||||
/// Create a new handler with hyper incoming request
|
||||
pub fn new(req: &'req Request<Incoming>, host: &'static SettingHost) -> Result<Self> {
|
||||
let req_path = req.uri().path();
|
||||
// find route path
|
||||
let (router, assets_path) = find_route(req_path, &host.route_map)?;
|
||||
|
||||
let candy = Self {
|
||||
pub fn new(req: Request<Incoming>, host: &'static SettingHost) -> Self {
|
||||
Self {
|
||||
req,
|
||||
res: Response::builder(),
|
||||
host,
|
||||
router,
|
||||
assets_path,
|
||||
};
|
||||
Ok(candy)
|
||||
router: None,
|
||||
assets_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Traverse the headers from config add to response
|
||||
/// these headers will be add before proxy headers and will override by proxy headers
|
||||
pub fn add_headers(&mut self) -> Result<()> {
|
||||
let headers = self
|
||||
.res
|
||||
@ -76,18 +75,26 @@ impl<'req> CandyHandler<'req> {
|
||||
let server = format!("{}/{}", NAME, VERSION);
|
||||
headers.insert("Server", server.parse()?);
|
||||
// config headers overrite
|
||||
if let Some(c_headers) = &self.host.headers {
|
||||
for (k, v) in c_headers {
|
||||
headers.insert(k.as_str(), v.parse()?);
|
||||
}
|
||||
let Some(c_headers) = &self.host.headers else {
|
||||
return Ok(());
|
||||
};
|
||||
for (k, v) in c_headers {
|
||||
headers.insert(k.as_str(), v.parse()?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle static file or reverse proxy
|
||||
pub async fn handle(self) -> CandyResponse {
|
||||
pub async fn handle(mut self) -> CandyResponse {
|
||||
let uri = self.req.uri().clone();
|
||||
let req_path = uri.path();
|
||||
// find route path
|
||||
let (router, assets_path) = find_route(req_path, &self.host.route_map)?;
|
||||
self.router = Some(router);
|
||||
self.assets_path = Some(assets_path);
|
||||
|
||||
// reverse proxy
|
||||
if self.router.proxy_pass.is_some() {
|
||||
if router.proxy_pass.is_some() {
|
||||
self.proxy().await
|
||||
} else {
|
||||
// static file
|
||||
@ -98,72 +105,55 @@ impl<'req> CandyHandler<'req> {
|
||||
/// Handle reverse proxy
|
||||
///
|
||||
/// Only use with the `proxy_pass` field in config
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn proxy(self) -> CandyResponse {
|
||||
let (router, assets_path) = (self.router, self.assets_path);
|
||||
let (req, res) = (self.req, self.res);
|
||||
let (router, assets_path) = (
|
||||
self.router
|
||||
.ok_or(Error::NotFound("handler router is empty".into()))?,
|
||||
self.assets_path
|
||||
.ok_or(Error::NotFound("handler assets_path is empty".into()))?,
|
||||
);
|
||||
let (req, mut res) = (self.req, self.res);
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let assets_path = if !assets_path.is_empty() {
|
||||
format!("/{assets_path}")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
// check on outside
|
||||
let proxy = router.proxy_pass.as_ref().ok_or(Error::Empty)?;
|
||||
let path_query = req.uri().query().unwrap_or(assets_path);
|
||||
let proxy = proxy.trim_end_matches('/');
|
||||
let path_query = parts.uri.query().unwrap_or("");
|
||||
let path_query = if !path_query.is_empty() {
|
||||
format!("?{path_query}")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let uri: hyper::Uri = format!("{}{}", proxy, path_query)
|
||||
let uri: hyper::Uri = format!("{proxy}{assets_path}{path_query}")
|
||||
.parse()
|
||||
.with_context(|| format!("parse proxy uri failed: {}", proxy))?;
|
||||
match uri.scheme_str() {
|
||||
Some("http") | Some("https") => {}
|
||||
_ => {
|
||||
return Err(Error::InternalServerError(anyhow!(
|
||||
"proxy uri scheme error: {}",
|
||||
uri
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let host = uri.host().ok_or(Error::InternalServerError(anyhow!(
|
||||
"proxy pass host incorrect"
|
||||
)))?;
|
||||
let port = uri.port_u16().unwrap_or(80);
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let stream = select! {
|
||||
stream = TcpStream::connect(&addr) => {
|
||||
stream.with_context(|| format!("connect to {} failed", addr))?
|
||||
let uri = uri.clone();
|
||||
debug!("proxy pass to: {uri}");
|
||||
let timeout = router.proxy_timeout;
|
||||
let body = body.collect().await?.to_bytes();
|
||||
let body = select! {
|
||||
body = client::get(uri, parts, body) => {
|
||||
body.with_context(|| "proxy body error")?
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(3)) => {
|
||||
return Err(anyhow!("connect upstream {} timeout", &addr).into());
|
||||
_ = tokio::time::sleep(Duration::from_secs(timeout.into())) => {
|
||||
return Err(anyhow!("connect upstream {host:?} timeout").into());
|
||||
}
|
||||
};
|
||||
let io = TokioIo::new(stream);
|
||||
|
||||
let (mut sender, conn) =
|
||||
hyper::client::conn::http1::handshake(io)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("cannot handshake with {}: {}", addr, err);
|
||||
anyhow!("{err}")
|
||||
})?;
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
println!("Connection failed: {:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let authority = uri
|
||||
.authority()
|
||||
.ok_or(anyhow!("proxy pass uri authority incorrect"))?;
|
||||
|
||||
let path = uri.path();
|
||||
let req = Request::builder()
|
||||
.uri(path)
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
|
||||
let client_res = sender
|
||||
.send_request(req)
|
||||
.await
|
||||
.with_context(|| "send request failed")?;
|
||||
let client_body = client_res.map_err(Error::HyperError).boxed();
|
||||
let res_body = res.body(client_body)?;
|
||||
res.headers_mut()
|
||||
.ok_or(Error::MissingHeader("missing response headers"))
|
||||
.with_context(|| "build response failed")?
|
||||
.extend(body.headers().clone());
|
||||
let res_body = res.body(body.map_err(Error::HyperError).boxed())?;
|
||||
Ok(res_body)
|
||||
}
|
||||
|
||||
@ -172,7 +162,12 @@ impl<'req> CandyHandler<'req> {
|
||||
///
|
||||
/// Only use with the `proxy_pass` field not in config
|
||||
pub async fn file(self) -> CandyResponse {
|
||||
let (router, assets_path) = (self.router, self.assets_path);
|
||||
let (router, assets_path) = (
|
||||
self.router
|
||||
.ok_or(Error::NotFound("handler router is empty".into()))?,
|
||||
self.assets_path
|
||||
.ok_or(Error::NotFound("handler assets_path is empty".into()))?,
|
||||
);
|
||||
let (req, res) = (self.req, self.res);
|
||||
|
||||
let req_method = req.method();
|
||||
@ -180,12 +175,13 @@ impl<'req> CandyHandler<'req> {
|
||||
// find resource local file path
|
||||
let mut path = None;
|
||||
for index in router.index.iter() {
|
||||
if let Some(root) = &router.root {
|
||||
let p = parse_assets_path(assets_path, root, index);
|
||||
if Path::new(&p).exists() {
|
||||
path = Some(p);
|
||||
break;
|
||||
}
|
||||
let Some(root) = &router.root else {
|
||||
continue;
|
||||
};
|
||||
let p = parse_assets_path(assets_path, root, index);
|
||||
if Path::new(&p).exists() {
|
||||
path = Some(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let path = match path {
|
||||
@ -201,12 +197,11 @@ impl<'req> CandyHandler<'req> {
|
||||
Method::POST => handle_get(req, res, &path).await?,
|
||||
// Return the 404 Not Found for other routes.
|
||||
_ => {
|
||||
if let Some(err_page) = &router.error_page {
|
||||
let res = res.status(err_page.status);
|
||||
handle_get(req, res, &err_page.page).await?
|
||||
} else {
|
||||
not_found()
|
||||
}
|
||||
let Some(err_page) = &router.error_page else {
|
||||
return Ok(not_found());
|
||||
};
|
||||
let res = res.status(err_page.status);
|
||||
handle_get(req, res, &err_page.page).await?
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
@ -221,13 +216,11 @@ impl<'req> CandyHandler<'req> {
|
||||
/// `path`: local file path
|
||||
pub async fn open_file(path: &str) -> Result<File> {
|
||||
// Open file for reading
|
||||
let file = File::open(path).await;
|
||||
let file = match file {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
error!("Unable to open file {err}");
|
||||
return Err(Error::NotFound(format!("path not found {}", path).into()));
|
||||
}
|
||||
let file = File::open(path).await.map_err(|err| {
|
||||
error!("Unable to open file {err}");
|
||||
});
|
||||
let Ok(file) = file else {
|
||||
return Err(Error::NotFound(format!("path not found {}", path).into()));
|
||||
};
|
||||
Ok(file)
|
||||
}
|
||||
@ -283,9 +276,9 @@ pub fn internal_server_error() -> Response<CandyBody<Bytes>> {
|
||||
// HTTP methods
|
||||
/// handle http get method
|
||||
/// read static file and check If-None-Match cache
|
||||
#[instrument]
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn handle_get(
|
||||
req: &Request<Incoming>,
|
||||
req: Request<Incoming>,
|
||||
mut res: Builder,
|
||||
path: &str,
|
||||
) -> Result<Response<CandyBody<Bytes>>> {
|
||||
@ -308,7 +301,7 @@ pub async fn handle_get(
|
||||
.extension()
|
||||
.ok_or(InternalServerError(anyhow!("read file extension failed")))?;
|
||||
|
||||
let settings = get_settings();
|
||||
let settings = get_settings()?;
|
||||
let content_type = settings.types.get(
|
||||
extension
|
||||
.to_str()
|
||||
@ -365,21 +358,19 @@ pub async fn handle_get(
|
||||
}
|
||||
|
||||
pub async fn handle_not_found(
|
||||
req: &Request<Incoming>,
|
||||
req: Request<Incoming>,
|
||||
res: Builder,
|
||||
router: &SettingRoute,
|
||||
assets_path: &str,
|
||||
) -> Result<Response<CandyBody<Bytes>>> {
|
||||
let res = if let Some(err_page) = &router.error_page {
|
||||
let res = res.status(err_page.status);
|
||||
if let Some(root) = &router.root {
|
||||
let path = parse_assets_path(assets_path, root, &err_page.page);
|
||||
handle_get(req, res, &path).await?
|
||||
} else {
|
||||
not_found()
|
||||
}
|
||||
} else {
|
||||
not_found()
|
||||
let Some(err_page) = &router.error_page else {
|
||||
return Ok(not_found());
|
||||
};
|
||||
let Some(root) = &router.root else {
|
||||
return Ok(not_found());
|
||||
};
|
||||
let res = res.status(err_page.status);
|
||||
let path = parse_assets_path(assets_path, root, &err_page.page);
|
||||
let res = handle_get(req, res, &path).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
12
src/main.rs
12
src/main.rs
@ -2,7 +2,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use clap::Parser;
|
||||
use config::Settings;
|
||||
use consts::COMPILER;
|
||||
use consts::{COMMIT, COMPILER};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{debug, info};
|
||||
|
||||
@ -11,6 +11,11 @@ use crate::{
|
||||
utils::init_logger,
|
||||
};
|
||||
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: MiMalloc = MiMalloc;
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod consts;
|
||||
@ -29,9 +34,10 @@ async fn main() -> Result<()> {
|
||||
.map_err(|err| anyhow!("init config failed {err:?}"))?;
|
||||
|
||||
// global config
|
||||
let settings = get_settings();
|
||||
let settings = get_settings().with_context(|| "get global settings failed")?;
|
||||
debug!("settings {:?}", settings);
|
||||
info!("{}/{} {}", NAME, VERSION, COMPILER);
|
||||
info!("{}/{} {}", NAME, VERSION, COMMIT);
|
||||
info!("{}", COMPILER);
|
||||
info!("OS: {} {}", OS, ARCH);
|
||||
|
||||
let mut servers = settings
|
||||
|
127
src/service.rs
127
src/service.rs
@ -1,15 +1,18 @@
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
pin::pin,
|
||||
time::{self, Duration, Instant},
|
||||
sync::Arc,
|
||||
time::{self, Duration},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::SettingHost,
|
||||
error::Error,
|
||||
http::{internal_server_error, not_found, CandyHandler},
|
||||
utils::{io_error, load_certs, load_private_key},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures_util::Future;
|
||||
use http::Request;
|
||||
use hyper::body::Incoming;
|
||||
@ -17,11 +20,13 @@ use hyper_util::{
|
||||
rt::{TokioExecutor, TokioIo},
|
||||
server::{self, graceful::GracefulShutdown},
|
||||
};
|
||||
use rustls::ServerConfig;
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
select,
|
||||
};
|
||||
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
impl SettingHost {
|
||||
@ -35,7 +40,43 @@ impl SettingHost {
|
||||
let graceful = server::graceful::GracefulShutdown::new();
|
||||
let mut ctrl_c = pin!(tokio::signal::ctrl_c());
|
||||
|
||||
// load ssl certificate
|
||||
let tls_acceptor: Option<TlsAcceptor> =
|
||||
if self.certificate.is_some() && self.certificate_key.is_some() {
|
||||
// Set a process wide default crypto provider.
|
||||
#[cfg(feature = "ring")]
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
#[cfg(feature = "aws-lc-rs")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
// Load public certificate.
|
||||
let certs = load_certs(
|
||||
self.certificate
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("cannot read certificate"))?,
|
||||
)?;
|
||||
info!("load ssl certificate success");
|
||||
// Load private key.
|
||||
let key = load_private_key(
|
||||
self.certificate_key
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("cannot read private key"))?,
|
||||
)?;
|
||||
info!("loading ssl private key success");
|
||||
// Build TLS configuration.
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.map_err(|e| io_error(e.to_string()))?;
|
||||
server_config.alpn_protocols =
|
||||
vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
let tls_acceptor = TlsAcceptor::from(Arc::new(server_config));
|
||||
Some(tls_acceptor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
loop {
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
tokio::select! {
|
||||
conn = listener.accept() => {
|
||||
let conn = match conn {
|
||||
@ -45,9 +86,8 @@ impl SettingHost {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
handle_connection(conn, self, &server, &graceful).await;
|
||||
handle_connection(conn, self, &server, &graceful, tls_acceptor).await;
|
||||
},
|
||||
|
||||
_ = ctrl_c.as_mut() => {
|
||||
drop(listener);
|
||||
info!("Ctrl-C received, starting shutdown");
|
||||
@ -69,6 +109,30 @@ impl SettingHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Use to handle connection
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// `$stream`: TcpStream or TlsStream
|
||||
/// `$server`: hyper_util server Builder
|
||||
/// `$service`: hyper service
|
||||
/// `$graceful`: hyper_util server graceful shutdown
|
||||
/// `$peer_addr`: SocketAddr
|
||||
macro_rules! handle_connection {
|
||||
($stream:expr, $server:expr, $service:expr, $graceful:expr, $peer_addr:expr) => {
|
||||
let stream = TokioIo::new(Box::pin($stream));
|
||||
let conn =
|
||||
$server.serve_connection_with_upgrades(stream, hyper::service::service_fn($service));
|
||||
let conn = $graceful.watch(conn.into_owned());
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
error!("connection error: {}", err);
|
||||
}
|
||||
debug!("connection dropped: {}", $peer_addr);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Handle tcp connection from client
|
||||
/// then use hyper service to handle response
|
||||
///
|
||||
@ -83,18 +147,24 @@ async fn handle_connection(
|
||||
host: &'static SettingHost,
|
||||
server: &server::conn::auto::Builder<TokioExecutor>,
|
||||
graceful: &GracefulShutdown,
|
||||
tls_acceptor: Option<TlsAcceptor>,
|
||||
) {
|
||||
let (stream, peer_addr) = conn;
|
||||
debug!("incomming connection accepted: {}", peer_addr);
|
||||
|
||||
let stream = TokioIo::new(Box::pin(stream));
|
||||
|
||||
let service = move |req: Request<Incoming>| async move {
|
||||
let start_time = time::Instant::now();
|
||||
let mut handler = CandyHandler::new(&req, host)?;
|
||||
let method = req.method().clone();
|
||||
let uri = req.uri().clone();
|
||||
let path = uri.path();
|
||||
let version = req.version();
|
||||
let mut handler = CandyHandler::new(req, host);
|
||||
// Connection handler in service_fn
|
||||
// then decide whether to handle proxy or static file based on config
|
||||
handler.add_headers()?;
|
||||
handler
|
||||
.add_headers()
|
||||
.map_err(|err| error!("add headers to response failed {}", err))
|
||||
.ok();
|
||||
let res = handler.handle().await;
|
||||
let response = match res {
|
||||
Ok(res) => res,
|
||||
@ -107,26 +177,33 @@ async fn handle_connection(
|
||||
internal_server_error()
|
||||
}
|
||||
};
|
||||
let end_time = (Instant::now() - start_time).as_micros() as f32;
|
||||
let end_time = end_time / 1000_f32;
|
||||
let method = &req.method();
|
||||
let path = &req.uri().path();
|
||||
let version = &req.version();
|
||||
let instant_elapsed = start_time.elapsed();
|
||||
let micros = instant_elapsed.as_micros();
|
||||
let millis = instant_elapsed.as_millis();
|
||||
let end_time = if micros >= 1000 {
|
||||
format!("{millis:.3}ms")
|
||||
} else {
|
||||
format!("{micros:.3}μs")
|
||||
};
|
||||
let res_status = response.status();
|
||||
info!(
|
||||
"\"{}\" {} {} {:?} {} {:.3}ms",
|
||||
peer_addr, method, path, version, res_status, end_time
|
||||
);
|
||||
info!("\"{peer_addr}\" {method} {path} {version:?} {res_status} {end_time}");
|
||||
anyhow::Ok(response)
|
||||
};
|
||||
|
||||
let conn = server.serve_connection_with_upgrades(stream, hyper::service::service_fn(service));
|
||||
let conn = graceful.watch(conn.into_owned());
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
error!("connection error: {}", err);
|
||||
}
|
||||
debug!("connection dropped: {}", peer_addr);
|
||||
});
|
||||
if host.certificate.is_some() && host.certificate_key.is_some() {
|
||||
let Some(tls_acceptor) = tls_acceptor else {
|
||||
warn!("tls_acceptor is None");
|
||||
return;
|
||||
};
|
||||
let tls_stream = match tls_acceptor.accept(stream).await {
|
||||
Ok(tls_stream) => tls_stream,
|
||||
Err(err) => {
|
||||
debug!("failed to perform tls handshake: {err:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
handle_connection!(tls_stream, server, service, graceful, peer_addr);
|
||||
} else {
|
||||
handle_connection!(stream, server, service, graceful, peer_addr);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
use std::{fs, io};
|
||||
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
@ -21,10 +24,10 @@ pub fn parse_assets_path(assets_path: &str, assets_root: &str, index_file: &str)
|
||||
format!("{}/{}", assets_root, assets_path)
|
||||
}
|
||||
str if !str.starts_with('/') => {
|
||||
format!("{}/{}{}", assets_root, assets_path, index_file)
|
||||
format!("{}/{}/{}", assets_root, assets_path, index_file)
|
||||
}
|
||||
_ => {
|
||||
format!("{}{}/{}", assets_root, assets_path, index_file)
|
||||
format!("{}/{}/{}", assets_root, assets_path, index_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,28 +50,59 @@ pub fn find_route<'a>(
|
||||
) -> Result<(&'a SettingRoute, &'a str)> {
|
||||
let not_found_err = format!("resource {} not found", &req_path);
|
||||
// /public/www/test
|
||||
// then find all stash's index
|
||||
let all_stash = &req_path
|
||||
.bytes()
|
||||
.enumerate()
|
||||
.filter(|(_, b)| *b == b'/')
|
||||
.map(|(index, _)| index + 1)
|
||||
.collect::<Vec<_>>();
|
||||
// loop the all_stash
|
||||
// /public/
|
||||
// /public/www/
|
||||
// convert req path to chars
|
||||
let all_chars = req_path.chars().collect::<Vec<_>>();
|
||||
let mut last_router = None;
|
||||
for index in all_stash {
|
||||
if let Some(router) = route_map.get(&req_path[..*index]) {
|
||||
last_router = Some((router, &req_path[*index..]));
|
||||
}
|
||||
// then loop all req path
|
||||
// until found the route
|
||||
// /public/www/test
|
||||
// /public/www/tes
|
||||
// /public/www/te
|
||||
// /public/www/t
|
||||
// /public/www/
|
||||
for (i, _) in all_chars.iter().enumerate().rev() {
|
||||
let index = i + 1;
|
||||
let path = &all_chars[..index];
|
||||
let path_str = path.iter().collect::<String>();
|
||||
let Some(router) = route_map.get(&path_str) else {
|
||||
continue;
|
||||
};
|
||||
last_router = Some((router, &req_path[index..]));
|
||||
break;
|
||||
}
|
||||
|
||||
let (router, assets_path) = last_router.ok_or(Error::NotFound(not_found_err.into()))?;
|
||||
debug!("router {:?}", &router);
|
||||
debug!("assets_path {assets_path}");
|
||||
Ok((router, assets_path))
|
||||
}
|
||||
|
||||
pub fn io_error(err: String) -> io::Error {
|
||||
io::Error::other(err)
|
||||
}
|
||||
|
||||
// Load public certificate from file.
|
||||
pub fn load_certs(filename: &str) -> io::Result<Vec<CertificateDer<'static>>> {
|
||||
// Open certificate file.
|
||||
let certfile = fs::File::open(filename)
|
||||
.map_err(|e| io_error(format!("failed to open {}: {}", filename, e)))?;
|
||||
let mut reader = io::BufReader::new(certfile);
|
||||
|
||||
// Load and return certificate.
|
||||
rustls_pemfile::certs(&mut reader).collect()
|
||||
}
|
||||
|
||||
// Load private key from file.
|
||||
pub fn load_private_key(filename: &str) -> io::Result<PrivateKeyDer<'static>> {
|
||||
// Open keyfile.
|
||||
let keyfile = fs::File::open(filename)
|
||||
.map_err(|e| io_error(format!("failed to open {}: {}", filename, e)))?;
|
||||
let mut reader = io::BufReader::new(keyfile);
|
||||
|
||||
// Load and return a single private key.
|
||||
rustls_pemfile::private_key(&mut reader).map(|key| key.unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
@ -89,6 +123,7 @@ mod tests {
|
||||
index: vec!["index.html".into()],
|
||||
error_page: None,
|
||||
proxy_pass: None,
|
||||
proxy_timeout: 10,
|
||||
};
|
||||
let map = BTreeMap::from([("/".to_string(), setting_route)]);
|
||||
let (_, assets_path) = find_route("/docs/home", &map).unwrap();
|
||||
|
Reference in New Issue
Block a user