Implement CLI configuration and runtime management

This commit is contained in:
thakares committed 2026-05-13 15:54:15 +05:30
1 parent 2ac0afa691
commit 3edea4bdff
14 files changed
+1362 -53

No files matched your search

Generated
+429 -1
View File
@@ -14,6 +14,65 @@ dependencies = [
"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]]
name = "arrayref"
version = "0.3.9"
@@ -173,17 +232,22 @@ version = "0.2.0"
dependencies = [
"axum",
"base64",
"clap",
"clap_complete",
"ed25519-dalek",
"hex",
"rand",
"rusqlite",
"serde",
"serde_json",
"serde_yaml",
"shared",
"tokio",
"toml",
"tower 0.4.13",
"tower-http",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
@@ -204,6 +268,62 @@ dependencies = [
"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]]
name = "const-oid"
version = "0.9.6"
@@ -234,6 +354,21 @@ dependencies = [
"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]]
name = "crypto-common"
version = "0.1.7"
@@ -281,6 +416,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -316,6 +460,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@@ -430,15 +580,27 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
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]]
name = "hex"
version = "0.4.3"
@@ -531,6 +693,22 @@ dependencies = [
"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]]
name = "itoa"
version = "1.0.18"
@@ -572,6 +750,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -587,6 +771,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "matchit"
version = "0.7.3"
@@ -635,12 +828,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -692,6 +897,12 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -758,6 +969,23 @@ dependencies = [
"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]]
name = "rusqlite"
version = "0.31.0"
@@ -781,6 +1009,19 @@ dependencies = [
"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]]
name = "rustversion"
version = "1.0.22"
@@ -870,6 +1111,15 @@ dependencies = [
"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]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -882,6 +1132,19 @@ dependencies = [
"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]]
name = "sha2"
version = "0.10.9"
@@ -972,12 +1235,24 @@ dependencies = [
"der",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "2.0.117"
@@ -995,6 +1270,36 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "thread_local"
version = "1.1.9"
@@ -1004,6 +1309,37 @@ dependencies = [
"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]]
name = "tokio"
version = "1.52.2"
@@ -1045,6 +1381,47 @@ dependencies = [
"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]]
name = "tower"
version = "0.4.13"
@@ -1121,6 +1498,19 @@ dependencies = [
"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]]
name = "tracing-attributes"
version = "0.1.31"
@@ -1153,18 +1543,35 @@ dependencies = [
"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]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
@@ -1185,6 +1592,18 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "valuable"
version = "0.1.1"
@@ -1269,6 +1688,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
+7 -3
View File
@@ -4,7 +4,7 @@ WORKDIR /app
COPY . .
RUN cargo build -p server --release
RUN cargo build -p chronoseal-server --bin chronoseal --release
FROM debian:bookworm-slim
@@ -14,10 +14,14 @@ RUN apt-get update && apt-get install -y \
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
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
View File
@@ -1,6 +1,8 @@
[Unit]
Description=ChronoSeal Anti-Bot Service
After=network.target
Description=ChronoSeal cryptographic browser attestation service
Documentation=https://chronoseal.rs
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
@@ -8,28 +10,58 @@ Type=simple
User=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
TimeoutStopSec=30
KillSignal=SIGTERM
RuntimeDirectory=chronoseal
RuntimeDirectoryMode=0750
StateDirectory=chronoseal
StateDirectoryMode=0750
LogsDirectory=chronoseal
LogsDirectoryMode=0750
ConfigurationDirectory=chronoseal
ConfigurationDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectHome=read-only
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
PrivateDevices=true
PrivateIPC=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
LockPersonality=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
+5
View File
@@ -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
View File
@@ -1,10 +1,16 @@
#!/bin/bash
set -e
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "Building WASM..."
cd ../wasm
wasm-pack build --target web
mv pkg ../frontend/pkg
cd "$ROOT/wasm"
wasm-pack build --target web --release
rm -rf "$ROOT/frontend/pkg"
mv pkg "$ROOT/frontend/pkg"
echo "Building server..."
cd ../server
cargo build --release
echo "Done."
cd "$ROOT"
cargo build -p chronoseal-server --bin chronoseal --release
echo "Done."
+52
View File
@@ -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
View File
@@ -1,5 +1,12 @@
#!/bin/bash
bash build.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
"$ROOT/scripts/build.sh"
echo "Release artifacts:"
echo " - server/target/release/antibot-server"
echo " - frontend/ (including pkg/)"
echo " - target/release/chronoseal"
echo " - frontend/ (including pkg/)"
echo " - chronoseal.service"
echo " - scripts/install.sh"
+10 -1
View File
@@ -3,15 +3,24 @@ name = "chronoseal-server"
version = "0.2.0"
edition = "2021"
[[bin]]
name = "chronoseal"
path = "src/main.rs"
[dependencies]
shared = { path = "../shared" }
axum = "0.7"
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
toml = "0.8"
rusqlite = { version = "0.31", features = ["bundled"] }
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
hex = "0.4"
+132
View File
@@ -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,
}
+159
View File
@@ -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
View File
@@ -1,47 +1,143 @@
mod cleanup;
mod cli;
mod config;
mod crypto;
mod fingerprint;
mod middleware;
mod output;
mod ratelimit;
mod routes;
mod runtime;
mod session;
mod storage;
mod trust;
mod vm;
use axum::Router;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;
use session::AppState;
use clap::{CommandFactory, Parser};
use cli::{Cli, Command, ConfigCommand, GenerateCommand};
use config::Config;
use std::path::PathBuf;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[tokio::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");
let state = Arc::new(AppState {
db: Mutex::new(conn),
rate_limiter: Mutex::new(ratelimit::RateLimiter::new(
shared::constants::RATE_LIMIT_COUNT,
shared::constants::RATE_LIMIT_WINDOW_SECS,
)),
});
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
let log_file = log_file_for_command(&cli);
let _log_guard = init_logging(log_filter, log_file)?;
// Periodic cleanup
let bg_state = state.clone();
tokio::spawn(async move { cleanup::cleanup_loop(bg_state).await });
match &cli.command {
None | Some(Command::Run(_)) => {
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()
.route("/init", axum::routing::post(routes::init::handler))
.route("/hb", axum::routing::post(routes::heartbeat::handler))
.nest_service("/", tower_http::services::ServeDir::new("../frontend"))
.layer(tower_http::cors::CorsLayer::permissive())
.layer(axum::middleware::from_fn(middleware::log_request))
.with_state(state);
fn log_file_for_command(cli: &Cli) -> Option<PathBuf> {
match &cli.command {
None => Config::load(cli.globals.config.as_deref())
.ok()
.and_then(|config| config.log_file),
Some(Command::Run(args)) => {
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();
info!("Server running on :3000");
axum::serve(listener, app).await.unwrap();
}
fn init_logging(
filter: &str,
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)
}
}
+18
View File
@@ -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;
}
+323
View File
@@ -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
View File
@@ -1,8 +1,26 @@
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn init_db() -> Result<Connection, rusqlite::Error> {
let conn = Connection::open_in_memory()?;
#[derive(Debug, Clone, Serialize, Deserialize)]
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(
"CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
@@ -18,6 +36,26 @@ pub fn init_db() -> Result<Connection, rusqlite::Error> {
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 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
}
}