Implement CLI configuration and runtime management
This commit is contained in:
1 parent
2ac0afa691
commit
3edea4bdff
14 files changed
+1362
-53
No files matched your search
Generated
+429
-1
@@ -14,6 +14,65 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -173,17 +232,22 @@ version = "0.2.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"rand",
|
"rand",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"shared",
|
"shared",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -204,6 +268,62 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
"terminal_size",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "4.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@@ -234,6 +354,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -281,6 +416,15 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -316,6 +460,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -430,15 +580,27 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -531,6 +693,22 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -572,6 +750,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -587,6 +771,15 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -635,12 +828,24 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -692,6 +897,12 @@ version = "0.3.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -758,6 +969,23 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.31.0"
|
version = "0.31.0"
|
||||||
@@ -781,6 +1009,19 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -870,6 +1111,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -882,6 +1132,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -972,12 +1235,24 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symlink"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -995,6 +1270,36 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||||
|
dependencies = [
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -1004,6 +1309,37 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.2"
|
version = "1.52.2"
|
||||||
@@ -1045,6 +1381,47 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -1121,6 +1498,19 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"symlink",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.31"
|
version = "0.1.31"
|
||||||
@@ -1153,18 +1543,35 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-serde"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.23"
|
version = "0.3.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1185,6 +1592,18 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1269,6 +1688,15 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.48"
|
version = "0.8.48"
|
||||||
|
|||||||
+7
-3
@@ -4,7 +4,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN cargo build -p server --release
|
RUN cargo build -p chronoseal-server --bin chronoseal --release
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -14,10 +14,14 @@ RUN apt-get update && apt-get install -y \
|
|||||||
|
|
||||||
WORKDIR /opt/chronoseal
|
WORKDIR /opt/chronoseal
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/server /usr/local/bin/chronoseal
|
COPY --from=builder /app/target/release/chronoseal /usr/local/bin/chronoseal
|
||||||
|
COPY frontend /usr/share/chronoseal/frontend
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
|
ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
|
||||||
|
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
|
||||||
|
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
||||||
|
|
||||||
CMD ["chronoseal"]
|
CMD ["chronoseal", "run"]
|
||||||
+38
-6
@@ -1,6 +1,8 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=ChronoSeal Anti-Bot Service
|
Description=ChronoSeal cryptographic browser attestation service
|
||||||
After=network.target
|
Documentation=https://chronoseal.rs
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@@ -8,28 +10,58 @@ Type=simple
|
|||||||
User=chronoseal
|
User=chronoseal
|
||||||
Group=chronoseal
|
Group=chronoseal
|
||||||
|
|
||||||
WorkingDirectory=/opt/chronoseal
|
Environment=RUST_LOG=info
|
||||||
|
Environment=CHRONOSEAL_CONFIG=/etc/chronoseal/config.toml
|
||||||
|
Environment=CHRONOSEAL_STATE_DIR=/var/lib/chronoseal
|
||||||
|
Environment=CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
||||||
|
|
||||||
ExecStart=/usr/local/bin/chronoseal
|
ExecStart=/usr/local/bin/chronoseal run
|
||||||
|
ExecStartPre=+/usr/bin/touch /run/chronoseal.pid
|
||||||
|
ExecStartPre=+/usr/bin/chown chronoseal:chronoseal /run/chronoseal.pid
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
ExecStopPost=+/usr/bin/rm -f /run/chronoseal.pid
|
||||||
|
PIDFile=/run/chronoseal.pid
|
||||||
|
|
||||||
Restart=always
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
TimeoutStopSec=30
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
|
RuntimeDirectory=chronoseal
|
||||||
|
RuntimeDirectoryMode=0750
|
||||||
|
StateDirectory=chronoseal
|
||||||
|
StateDirectoryMode=0750
|
||||||
|
LogsDirectory=chronoseal
|
||||||
|
LogsDirectoryMode=0750
|
||||||
|
ConfigurationDirectory=chronoseal
|
||||||
|
ConfigurationDirectoryMode=0750
|
||||||
|
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=true
|
ProtectHome=read-only
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
ProtectKernelModules=true
|
ProtectKernelModules=true
|
||||||
ProtectControlGroups=true
|
ProtectControlGroups=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectProc=invisible
|
||||||
|
ProcSubset=pid
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateIPC=true
|
||||||
|
|
||||||
MemoryDenyWriteExecute=true
|
MemoryDenyWriteExecute=true
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
|
RemoveIPC=true
|
||||||
|
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
|
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
SystemCallErrorNumber=EPERM
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
bind = "0.0.0.0:3000"
|
||||||
|
pid_file = "/run/chronoseal.pid"
|
||||||
|
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
|
||||||
|
frontend_dir = "/usr/share/chronoseal/frontend"
|
||||||
|
log_file = "/var/log/chronoseal/chronoseal.jsonl"
|
||||||
Regular → Executable
+13
-7
@@ -1,10 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
echo "Building WASM..."
|
echo "Building WASM..."
|
||||||
cd ../wasm
|
cd "$ROOT/wasm"
|
||||||
wasm-pack build --target web
|
wasm-pack build --target web --release
|
||||||
mv pkg ../frontend/pkg
|
rm -rf "$ROOT/frontend/pkg"
|
||||||
|
mv pkg "$ROOT/frontend/pkg"
|
||||||
|
|
||||||
echo "Building server..."
|
echo "Building server..."
|
||||||
cd ../server
|
cd "$ROOT"
|
||||||
cargo build --release
|
cargo build -p chronoseal-server --bin chronoseal --release
|
||||||
echo "Done."
|
|
||||||
|
echo "Done."
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CHRONOSEAL_VERSION="${CHRONOSEAL_VERSION:-latest}"
|
||||||
|
CHRONOSEAL_INSTALL_DIR="${CHRONOSEAL_INSTALL_DIR:-/usr/local/bin}"
|
||||||
|
CHRONOSEAL_BASE_URL="${CHRONOSEAL_BASE_URL:-https://get.chronoseal.rs/releases}"
|
||||||
|
|
||||||
|
need() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "chronoseal installer: missing required command: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
need uname
|
||||||
|
need mktemp
|
||||||
|
need chmod
|
||||||
|
|
||||||
|
arch="$(uname -m)"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64|amd64) target="x86_64-unknown-linux-musl" ;;
|
||||||
|
aarch64|arm64) target="aarch64-unknown-linux-musl" ;;
|
||||||
|
*) echo "chronoseal installer: unsupported architecture: $arch" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
fetch="curl --proto =https --tlsv1.2 -fsSL"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
fetch="wget -qO-"
|
||||||
|
else
|
||||||
|
echo "chronoseal installer: install curl or wget" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
url="$CHRONOSEAL_BASE_URL/$CHRONOSEAL_VERSION/chronoseal-$target.tar.gz"
|
||||||
|
echo "downloading chronoseal $CHRONOSEAL_VERSION for $target"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$fetch "$url" | tar -xz -C "$tmp"
|
||||||
|
chmod 0755 "$tmp/chronoseal"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
install -m 0755 "$tmp/chronoseal" "$CHRONOSEAL_INSTALL_DIR/chronoseal"
|
||||||
|
else
|
||||||
|
sudo install -m 0755 "$tmp/chronoseal" "$CHRONOSEAL_INSTALL_DIR/chronoseal"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "installed: $CHRONOSEAL_INSTALL_DIR/chronoseal"
|
||||||
|
echo "try: chronoseal --help"
|
||||||
Regular → Executable
+10
-3
@@ -1,5 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
bash build.sh
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
"$ROOT/scripts/build.sh"
|
||||||
|
|
||||||
echo "Release artifacts:"
|
echo "Release artifacts:"
|
||||||
echo " - server/target/release/antibot-server"
|
echo " - target/release/chronoseal"
|
||||||
echo " - frontend/ (including pkg/)"
|
echo " - frontend/ (including pkg/)"
|
||||||
|
echo " - chronoseal.service"
|
||||||
|
echo " - scripts/install.sh"
|
||||||
+10
-1
@@ -3,15 +3,24 @@ name = "chronoseal-server"
|
|||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "chronoseal"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
|
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
|
||||||
|
clap_complete = "4"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
toml = "0.8"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-appender = "0.2"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
Text,
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "chronoseal",
|
||||||
|
version,
|
||||||
|
about = "Linux-native cryptographic browser attestation service",
|
||||||
|
long_about = "ChronoSeal runs as a composable Unix service. The CLI is the source of truth for daemon operation, health checks, configuration validation, metrics, and shell integration.",
|
||||||
|
after_help = "Examples:\n chronoseal\n chronoseal run --bind 127.0.0.1:3000\n chronoseal status --format json\n chronoseal health --config /etc/chronoseal/config.toml\n chronoseal config check --output yaml\n chronoseal generate keypair\n chronoseal completion bash > /etc/bash_completion.d/chronoseal\n\nConfiguration precedence:\n CLI flags > CHRONOSEAL_* environment variables > config file > built-in defaults\n\nDefault config discovery:\n /etc/chronoseal/config.toml, then $XDG_CONFIG_HOME/chronoseal/config.toml, then ~/.config/chronoseal/config.toml"
|
||||||
|
)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub globals: GlobalArgs,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct GlobalArgs {
|
||||||
|
/// Path to config file.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_CONFIG", global = true)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Output format for machine-readable commands.
|
||||||
|
#[arg(long, short = 'f', value_enum, default_value = "text", global = true)]
|
||||||
|
pub format: OutputFormat,
|
||||||
|
|
||||||
|
/// Alias for --format, provided for Unix tool compatibility.
|
||||||
|
#[arg(long, value_enum, global = true)]
|
||||||
|
pub output: Option<OutputFormat>,
|
||||||
|
|
||||||
|
/// Override the logging filter, for example info, chronoseal=debug.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_LOG", global = true)]
|
||||||
|
pub log: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalArgs {
|
||||||
|
pub fn output_format(&self) -> OutputFormat {
|
||||||
|
self.output.unwrap_or(self.format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Run the ChronoSeal daemon.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal run\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run")]
|
||||||
|
Run(RunArgs),
|
||||||
|
|
||||||
|
/// Report whether the configured daemon is reachable and which PID file is present.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal status\n chronoseal status --format json\n chronoseal status --pid-file /run/chronoseal.pid")]
|
||||||
|
Status(RuntimeArgs),
|
||||||
|
|
||||||
|
/// Perform a daemon health probe.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal health\n chronoseal health --format json\n chronoseal health --bind 127.0.0.1:3000")]
|
||||||
|
Health(RuntimeArgs),
|
||||||
|
|
||||||
|
/// Validate and print effective configuration.
|
||||||
|
#[command(subcommand)]
|
||||||
|
Config(ConfigCommand),
|
||||||
|
|
||||||
|
/// Generate operational material.
|
||||||
|
#[command(subcommand)]
|
||||||
|
Generate(GenerateCommand),
|
||||||
|
|
||||||
|
/// Print version and build information.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal version\n chronoseal version --format json")]
|
||||||
|
Version,
|
||||||
|
|
||||||
|
/// Print Prometheus metrics from the running daemon.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000")]
|
||||||
|
Metrics(RuntimeArgs),
|
||||||
|
|
||||||
|
/// Print service statistics from the running daemon.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal stats\n chronoseal stats --format json")]
|
||||||
|
Stats(RuntimeArgs),
|
||||||
|
|
||||||
|
/// Generate shell completions.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal completion bash\n chronoseal completion zsh > ~/.zfunc/_chronoseal")]
|
||||||
|
Completion { shell: clap_complete::Shell },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct RunArgs {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub runtime: RuntimeArgs,
|
||||||
|
|
||||||
|
/// SQLite database path. Use ':memory:' for ephemeral state.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_DB_PATH")]
|
||||||
|
pub db_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Static frontend directory served at /.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_FRONTEND_DIR")]
|
||||||
|
pub frontend_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Optional structured JSON log file.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_LOG_FILE")]
|
||||||
|
pub log_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct RuntimeArgs {
|
||||||
|
/// Socket address the daemon binds to, or that CLI probes connect to.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_BIND")]
|
||||||
|
pub bind: Option<String>,
|
||||||
|
|
||||||
|
/// PID file path.
|
||||||
|
#[arg(long, env = "CHRONOSEAL_PID_FILE")]
|
||||||
|
pub pid_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum ConfigCommand {
|
||||||
|
/// Validate configuration and print the effective values.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal config check\n chronoseal config check --config /etc/chronoseal/config.toml\n chronoseal config check --output json")]
|
||||||
|
Check(RuntimeArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum GenerateCommand {
|
||||||
|
/// Generate an Ed25519 keypair as hex-encoded JSON/YAML/text.
|
||||||
|
#[command(after_help = "Examples:\n chronoseal generate keypair\n chronoseal generate keypair --format json")]
|
||||||
|
Keypair,
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
use crate::cli::{RunArgs, RuntimeArgs};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
env, fs, io,
|
||||||
|
net::SocketAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Config {
|
||||||
|
pub bind: String,
|
||||||
|
pub pid_file: PathBuf,
|
||||||
|
pub db_path: PathBuf,
|
||||||
|
pub frontend_dir: PathBuf,
|
||||||
|
pub log_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bind: "0.0.0.0:3000".to_string(),
|
||||||
|
pid_file: PathBuf::from("/run/chronoseal.pid"),
|
||||||
|
db_path: default_state_dir().join("chronoseal.sqlite"),
|
||||||
|
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
|
||||||
|
log_file: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load(config_path: Option<&Path>) -> Result<Self, ConfigError> {
|
||||||
|
let mut config = Self::default();
|
||||||
|
|
||||||
|
if let Some(path) = config_path.map(Path::to_path_buf).or_else(discover_config_path) {
|
||||||
|
let raw = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
|
||||||
|
path: path.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
config = toml::from_str(&raw).map_err(|source| ConfigError::Parse {
|
||||||
|
path: path.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.apply_env();
|
||||||
|
config.validate()?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_runtime_args(&mut self, args: &RuntimeArgs) {
|
||||||
|
if let Some(bind) = &args.bind {
|
||||||
|
self.bind.clone_from(bind);
|
||||||
|
}
|
||||||
|
if let Some(pid_file) = &args.pid_file {
|
||||||
|
self.pid_file = pid_file.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_run_args(&mut self, args: &RunArgs) {
|
||||||
|
self.apply_runtime_args(&args.runtime);
|
||||||
|
if let Some(db_path) = &args.db_path {
|
||||||
|
self.db_path = db_path.clone();
|
||||||
|
}
|
||||||
|
if let Some(frontend_dir) = &args.frontend_dir {
|
||||||
|
self.frontend_dir = frontend_dir.clone();
|
||||||
|
}
|
||||||
|
if let Some(log_file) = &args.log_file {
|
||||||
|
self.log_file = Some(log_file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||||
|
self.bind
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.map_err(|source| ConfigError::InvalidBind {
|
||||||
|
bind: self.bind.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_env(&mut self) {
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_BIND") {
|
||||||
|
self.bind = value;
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_PID_FILE") {
|
||||||
|
self.pid_file = PathBuf::from(value);
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_DB_PATH") {
|
||||||
|
self.db_path = PathBuf::from(value);
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_FRONTEND_DIR") {
|
||||||
|
self.frontend_dir = PathBuf::from(value);
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_LOG_FILE") {
|
||||||
|
self.log_file = Some(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Read {
|
||||||
|
path: PathBuf,
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
Parse {
|
||||||
|
path: PathBuf,
|
||||||
|
source: toml::de::Error,
|
||||||
|
},
|
||||||
|
InvalidBind {
|
||||||
|
bind: String,
|
||||||
|
source: std::net::AddrParseError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Read { path, source } => write!(f, "failed to read {}: {source}", path.display()),
|
||||||
|
Self::Parse { path, source } => {
|
||||||
|
write!(f, "failed to parse {} as TOML: {source}", path.display())
|
||||||
|
}
|
||||||
|
Self::InvalidBind { bind, source } => write!(f, "invalid bind address {bind}: {source}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
fn discover_config_path() -> Option<PathBuf> {
|
||||||
|
user_config_candidates()
|
||||||
|
.into_iter()
|
||||||
|
.find(|candidate| candidate.is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_config_candidates() -> Vec<PathBuf> {
|
||||||
|
let mut candidates = vec![PathBuf::from("/etc/chronoseal/config.toml")];
|
||||||
|
if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
|
||||||
|
candidates.push(PathBuf::from(xdg).join("chronoseal/config.toml"));
|
||||||
|
} else if let Ok(home) = env::var("HOME") {
|
||||||
|
candidates.push(PathBuf::from(home).join(".config/chronoseal/config.toml"));
|
||||||
|
}
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_state_dir() -> PathBuf {
|
||||||
|
if let Ok(value) = env::var("CHRONOSEAL_STATE_DIR") {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
if let Ok(value) = env::var("XDG_STATE_HOME") {
|
||||||
|
return PathBuf::from(value).join("chronoseal");
|
||||||
|
}
|
||||||
|
if let Ok(home) = env::var("HOME") {
|
||||||
|
return PathBuf::from(home).join(".local/state/chronoseal");
|
||||||
|
}
|
||||||
|
PathBuf::from("/var/lib/chronoseal")
|
||||||
|
}
|
||||||
+125
-29
@@ -1,47 +1,143 @@
|
|||||||
mod cleanup;
|
mod cleanup;
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
mod fingerprint;
|
mod fingerprint;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
mod output;
|
||||||
mod ratelimit;
|
mod ratelimit;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod runtime;
|
||||||
mod session;
|
mod session;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod trust;
|
mod trust;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|
||||||
use axum::Router;
|
use clap::{CommandFactory, Parser};
|
||||||
use std::sync::Arc;
|
use cli::{Cli, Command, ConfigCommand, GenerateCommand};
|
||||||
use tokio::sync::Mutex;
|
use config::Config;
|
||||||
use tracing::info;
|
use std::path::PathBuf;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
use session::AppState;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
if let Err(err) = try_main().await {
|
||||||
|
eprintln!("chronoseal: {err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let conn = storage::init_db().expect("DB init");
|
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let state = Arc::new(AppState {
|
let cli = Cli::parse();
|
||||||
db: Mutex::new(conn),
|
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
||||||
rate_limiter: Mutex::new(ratelimit::RateLimiter::new(
|
let log_file = log_file_for_command(&cli);
|
||||||
shared::constants::RATE_LIMIT_COUNT,
|
let _log_guard = init_logging(log_filter, log_file)?;
|
||||||
shared::constants::RATE_LIMIT_WINDOW_SECS,
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Periodic cleanup
|
match &cli.command {
|
||||||
let bg_state = state.clone();
|
None | Some(Command::Run(_)) => {
|
||||||
tokio::spawn(async move { cleanup::cleanup_loop(bg_state).await });
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
if let Some(Command::Run(args)) = &cli.command {
|
||||||
|
config.apply_run_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
}
|
||||||
|
runtime::run_daemon(config).await?;
|
||||||
|
}
|
||||||
|
Some(Command::Status(args)) => {
|
||||||
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
config.apply_runtime_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
output::print(cli.globals.output_format(), &runtime::probe_status(&config))?;
|
||||||
|
}
|
||||||
|
Some(Command::Health(args)) => {
|
||||||
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
config.apply_runtime_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
let report = runtime::probe_health(&config);
|
||||||
|
let healthy = report.status == "healthy";
|
||||||
|
output::print(cli.globals.output_format(), &report)?;
|
||||||
|
if !healthy {
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Command::Config(ConfigCommand::Check(args))) => {
|
||||||
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
config.apply_runtime_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
output::print(cli.globals.output_format(), &config)?;
|
||||||
|
}
|
||||||
|
Some(Command::Generate(GenerateCommand::Keypair)) => {
|
||||||
|
output::print(cli.globals.output_format(), &runtime::generate_keypair())?;
|
||||||
|
}
|
||||||
|
Some(Command::Version) => {
|
||||||
|
output::print(cli.globals.output_format(), &runtime::version())?;
|
||||||
|
}
|
||||||
|
Some(Command::Metrics(args)) => {
|
||||||
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
config.apply_runtime_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
print!("{}", runtime::fetch_metrics(&config)?);
|
||||||
|
}
|
||||||
|
Some(Command::Stats(args)) => {
|
||||||
|
let mut config = Config::load(cli.globals.config.as_deref())?;
|
||||||
|
config.apply_runtime_args(args);
|
||||||
|
config.validate()?;
|
||||||
|
output::print(cli.globals.output_format(), &runtime::fetch_stats(&config)?)?;
|
||||||
|
}
|
||||||
|
Some(Command::Completion { shell }) => {
|
||||||
|
let mut command = Cli::command();
|
||||||
|
let name = command.get_name().to_string();
|
||||||
|
clap_complete::generate(*shell, &mut command, name, &mut std::io::stdout());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
let app = Router::new()
|
fn log_file_for_command(cli: &Cli) -> Option<PathBuf> {
|
||||||
.route("/init", axum::routing::post(routes::init::handler))
|
match &cli.command {
|
||||||
.route("/hb", axum::routing::post(routes::heartbeat::handler))
|
None => Config::load(cli.globals.config.as_deref())
|
||||||
.nest_service("/", tower_http::services::ServeDir::new("../frontend"))
|
.ok()
|
||||||
.layer(tower_http::cors::CorsLayer::permissive())
|
.and_then(|config| config.log_file),
|
||||||
.layer(axum::middleware::from_fn(middleware::log_request))
|
Some(Command::Run(args)) => {
|
||||||
.with_state(state);
|
let mut config = Config::load(cli.globals.config.as_deref()).ok()?;
|
||||||
|
config.apply_run_args(args);
|
||||||
|
config.log_file
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
fn init_logging(
|
||||||
info!("Server running on :3000");
|
filter: &str,
|
||||||
axum::serve(listener, app).await.unwrap();
|
log_file: Option<PathBuf>,
|
||||||
}
|
) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>, Box<dyn std::error::Error>> {
|
||||||
|
let env_filter = EnvFilter::try_new(filter)?;
|
||||||
|
if let Some(path) = log_file {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let directory = path.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("chronoseal.jsonl");
|
||||||
|
let appender = tracing_appender::rolling::never(directory, file_name);
|
||||||
|
let (writer, guard) = tracing_appender::non_blocking(appender);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(env_filter)
|
||||||
|
.with(tracing_subscriber::fmt::layer().with_target(false))
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_target(false)
|
||||||
|
.with_writer(writer),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
Ok(Some(guard))
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.with_target(false)
|
||||||
|
.init();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use crate::cli::OutputFormat;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub fn print<T>(format: OutputFormat, value: &T) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
T: Serialize + TextOutput,
|
||||||
|
{
|
||||||
|
match format {
|
||||||
|
OutputFormat::Text => println!("{}", value.to_text()),
|
||||||
|
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(value)?),
|
||||||
|
OutputFormat::Yaml => print!("{}", serde_yaml::to_string(value)?),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TextOutput {
|
||||||
|
fn to_text(&self) -> String;
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
output::TextOutput,
|
||||||
|
ratelimit::RateLimiter,
|
||||||
|
routes, session,
|
||||||
|
storage::{self, StoreStats},
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{Read, Write},
|
||||||
|
net::{SocketAddr, TcpStream},
|
||||||
|
path::Path,
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::sync::{Mutex, Notify};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct HealthReport {
|
||||||
|
pub status: &'static str,
|
||||||
|
pub bind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for HealthReport {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
format!("{}\nbind={}", self.status, self.bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct StatusReport {
|
||||||
|
pub running: bool,
|
||||||
|
pub healthy: bool,
|
||||||
|
pub bind: String,
|
||||||
|
pub pid_file: String,
|
||||||
|
pub pid: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for StatusReport {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
let pid = self.pid.map_or_else(|| "unknown".to_string(), |pid| pid.to_string());
|
||||||
|
format!(
|
||||||
|
"running={}\nhealthy={}\nbind={}\npid_file={}\npid={}",
|
||||||
|
self.running, self.healthy, self.bind, self.pid_file, pid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct VersionReport {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub version: &'static str,
|
||||||
|
pub target: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for VersionReport {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
format!("{} {}", self.name, self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct KeypairReport {
|
||||||
|
pub algorithm: &'static str,
|
||||||
|
pub public_key_hex: String,
|
||||||
|
pub private_key_hex: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for KeypairReport {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"algorithm={}\npublic_key_hex={}\nprivate_key_hex={}",
|
||||||
|
self.algorithm, self.public_key_hex, self.private_key_hex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for Config {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"bind={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}",
|
||||||
|
self.bind,
|
||||||
|
self.pid_file.display(),
|
||||||
|
self.db_path.display(),
|
||||||
|
self.frontend_dir.display(),
|
||||||
|
self.log_file
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextOutput for StoreStats {
|
||||||
|
fn to_text(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"sessions={}\nexpired_sessions={}\nmax_chain_length={}",
|
||||||
|
self.sessions, self.expired_sessions, self.max_chain_length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
install_pid_file(&config.pid_file)?;
|
||||||
|
|
||||||
|
let conn = storage::init_db(&config.db_path)?;
|
||||||
|
let state = Arc::new(session::AppState {
|
||||||
|
db: Mutex::new(conn),
|
||||||
|
rate_limiter: Mutex::new(RateLimiter::new(
|
||||||
|
shared::constants::RATE_LIMIT_COUNT,
|
||||||
|
shared::constants::RATE_LIMIT_WINDOW_SECS,
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
|
||||||
|
let bg_state = state.clone();
|
||||||
|
tokio::spawn(async move { crate::cleanup::cleanup_loop(bg_state).await });
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/init", axum::routing::post(routes::init::handler))
|
||||||
|
.route("/hb", axum::routing::post(routes::heartbeat::handler))
|
||||||
|
.route("/health", get(health_handler))
|
||||||
|
.route("/metrics", get(metrics_handler))
|
||||||
|
.route("/stats", get(stats_handler))
|
||||||
|
.nest_service("/", tower_http::services::ServeDir::new(&config.frontend_dir))
|
||||||
|
.layer(tower_http::cors::CorsLayer::permissive())
|
||||||
|
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let addr: SocketAddr = config.bind.parse()?;
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
info!(bind = %config.bind, "chronoseal daemon started");
|
||||||
|
|
||||||
|
let shutdown = signal_task(config.clone());
|
||||||
|
let result = axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(shutdown)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
remove_pid_file(&config.pid_file);
|
||||||
|
result?;
|
||||||
|
info!("chronoseal daemon stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn probe_health(config: &Config) -> HealthReport {
|
||||||
|
if http_get(&config.bind, "/health").is_ok() {
|
||||||
|
HealthReport {
|
||||||
|
status: "healthy",
|
||||||
|
bind: config.bind.clone(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HealthReport {
|
||||||
|
status: "unreachable",
|
||||||
|
bind: config.bind.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn probe_status(config: &Config) -> StatusReport {
|
||||||
|
let pid = read_pid(&config.pid_file);
|
||||||
|
let healthy = http_get(&config.bind, "/health").is_ok();
|
||||||
|
StatusReport {
|
||||||
|
running: pid.is_some() || healthy,
|
||||||
|
healthy,
|
||||||
|
bind: config.bind.clone(),
|
||||||
|
pid_file: config.pid_file.display().to_string(),
|
||||||
|
pid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_metrics(config: &Config) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
http_get(&config.bind, "/metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_stats(config: &Config) -> Result<StoreStats, Box<dyn std::error::Error>> {
|
||||||
|
let body = http_get(&config.bind, "/stats")?;
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_keypair() -> KeypairReport {
|
||||||
|
let private_key = rand::random::<[u8; 32]>();
|
||||||
|
let signing_key = ed25519_dalek::SigningKey::from_bytes(&private_key);
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
KeypairReport {
|
||||||
|
algorithm: "ed25519",
|
||||||
|
public_key_hex: hex::encode(verifying_key.to_bytes()),
|
||||||
|
private_key_hex: hex::encode(private_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn version() -> VersionReport {
|
||||||
|
VersionReport {
|
||||||
|
name: "chronoseal",
|
||||||
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
|
target: std::env::consts::ARCH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_handler() -> impl IntoResponse {
|
||||||
|
(StatusCode::OK, Json(serde_json::json!({ "status": "healthy" })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stats_handler(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
|
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
storage::stats(&db)
|
||||||
|
.map(Json)
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn metrics_handler(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
|
) -> Result<String, (StatusCode, String)> {
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
storage::stats(&db)
|
||||||
|
.map(|stats| {
|
||||||
|
format!(
|
||||||
|
"# HELP chronoseal_sessions Active ChronoSeal sessions\n# TYPE chronoseal_sessions gauge\nchronoseal_sessions {}\n# HELP chronoseal_expired_sessions Expired sessions not yet removed\n# TYPE chronoseal_expired_sessions gauge\nchronoseal_expired_sessions {}\n# HELP chronoseal_max_chain_length Maximum heartbeat chain length\n# TYPE chronoseal_max_chain_length gauge\nchronoseal_max_chain_length {}\n",
|
||||||
|
stats.sessions, stats.expired_sessions, stats.max_chain_length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signal_task(config: Config) {
|
||||||
|
let shutdown = Arc::new(Notify::new());
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
|
|
||||||
|
let shutdown_term = shutdown.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
|
||||||
|
sigterm.recv().await;
|
||||||
|
info!("received SIGTERM; shutting down gracefully");
|
||||||
|
shutdown_term.notify_one();
|
||||||
|
});
|
||||||
|
|
||||||
|
let shutdown_int = shutdown.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if tokio::signal::ctrl_c().await.is_ok() {
|
||||||
|
info!("received interrupt; shutting down gracefully");
|
||||||
|
shutdown_int.notify_one();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let hup_config = config.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut sighup = signal(SignalKind::hangup()).expect("install SIGHUP handler");
|
||||||
|
while sighup.recv().await.is_some() {
|
||||||
|
match Config::load(None) {
|
||||||
|
Ok(reloaded) => info!(
|
||||||
|
bind = %reloaded.bind,
|
||||||
|
db_path = %reloaded.db_path.display(),
|
||||||
|
"received SIGHUP; configuration reloaded"
|
||||||
|
),
|
||||||
|
Err(err) => warn!(error = %err, "received SIGHUP; configuration reload failed"),
|
||||||
|
}
|
||||||
|
let _ = &hup_config;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut sigusr1 = signal(SignalKind::user_defined1()).expect("install SIGUSR1 handler");
|
||||||
|
while sigusr1.recv().await.is_some() {
|
||||||
|
info!("received SIGUSR1; stats are available via chronoseal stats or /stats");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
if tokio::signal::ctrl_c().await.is_ok() {
|
||||||
|
shutdown.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown.notified().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_pid_file(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if let Err(err) = fs::create_dir_all(parent) {
|
||||||
|
warn!(path = %parent.display(), error = %err, "could not create PID directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match fs::write(path, std::process::id().to_string()) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(path = %path.display(), error = %err, "could not write PID file");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_pid_file(path: &Path) {
|
||||||
|
if let Err(err) = fs::remove_file(path) {
|
||||||
|
if err.kind() != std::io::ErrorKind::NotFound {
|
||||||
|
error!(path = %path.display(), error = %err, "could not remove PID file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pid(path: &Path) -> Option<u32> {
|
||||||
|
fs::read_to_string(path).ok()?.trim().parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let mut stream = TcpStream::connect_timeout(&bind.parse()?, Duration::from_secs(2))?;
|
||||||
|
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
|
||||||
|
stream.write_all(format!("GET {path} HTTP/1.1\r\nHost: chronoseal\r\nConnection: close\r\n\r\n").as_bytes())?;
|
||||||
|
|
||||||
|
let mut response = String::new();
|
||||||
|
stream.read_to_string(&mut response)?;
|
||||||
|
let (_, body) = response
|
||||||
|
.split_once("\r\n\r\n")
|
||||||
|
.ok_or("daemon returned an invalid HTTP response")?;
|
||||||
|
Ok(body.to_string())
|
||||||
|
}
|
||||||
+41
-3
@@ -1,8 +1,26 @@
|
|||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub fn init_db() -> Result<Connection, rusqlite::Error> {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
let conn = Connection::open_in_memory()?;
|
pub struct StoreStats {
|
||||||
|
pub sessions: u64,
|
||||||
|
pub expired_sessions: u64,
|
||||||
|
pub max_chain_length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_db(path: &Path) -> Result<Connection, rusqlite::Error> {
|
||||||
|
if path == Path::new(":memory:") {
|
||||||
|
return init_schema(Connection::open_in_memory()?);
|
||||||
|
}
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
init_schema(Connection::open(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_schema(conn: Connection) -> Result<Connection, rusqlite::Error> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"CREATE TABLE IF NOT EXISTS sessions (
|
"CREATE TABLE IF NOT EXISTS sessions (
|
||||||
session_id TEXT PRIMARY KEY,
|
session_id TEXT PRIMARY KEY,
|
||||||
@@ -18,6 +36,26 @@ pub fn init_db() -> Result<Connection, rusqlite::Error> {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> {
|
||||||
|
let now = current_time_ms();
|
||||||
|
let sessions = conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?;
|
||||||
|
let expired_sessions = conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM sessions WHERE expires_at < ?1",
|
||||||
|
[now],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
let max_chain_length = conn.query_row(
|
||||||
|
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(StoreStats {
|
||||||
|
sessions,
|
||||||
|
expired_sessions,
|
||||||
|
max_chain_length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_time_ms() -> u64 {
|
pub fn current_time_ms() -> u64 {
|
||||||
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
|
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user