mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 18:58:43 +02:00
Façade Rust loki-rs : proxy devant le Go et mise à jour au démarrage
Premier lot de la migration vers Rust. Le binaire loki-rs : - serve : prend le port public (8090), lance `loki web 8091` en écoute locale et lui relaie tout — HTTP, flux SSE trame par trame, WebSocket. Les en-têtes de bout en bout passent intacts (Host, Authorization, Origin, Sec-Fetch-*, X-Forwarded-Proto : le Go en tire ses contrôles d'accès) ; X-Loki-Via entrant est effacé, un client ne doit jamais pouvoir se faire passer pour le tunnel. Go mort : relancé avec attente croissante, 502 lisible entre-temps. - self-update : lit la release « rolling », installe loki et loki-rs dans $LOKI_HOME/bin/<build>/ après vérification SHA-256, bascule par réécriture atomique de release.json. Les versions s'ordonnent par numéro de run CI ; une ABI d'image différente n'est jamais installée. Toute erreur laisse l'état précédent, le démarrage n'est jamais bloqué. Le HTTPS passe par curl (présent dans l'image) : pas de pile TLS ici. - which-bin : dossier d'où l'entrypoint prend les deux binaires, toujours ensemble. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
342f787eff
commit
0a6914c936
14 files changed
+2138
No files matched your search
Generated
+670
@@ -0,0 +1,670 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"hyper",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "loki-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["loki-rs"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = "thin"
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "loki-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.12.1"
|
||||
hex = "0.4.3"
|
||||
http-body-util = "0.1.5"
|
||||
hyper = { version = "1.12.0", features = ["http1", "server", "client"] }
|
||||
hyper-util = { version = "0.1.21", features = ["tokio", "server", "client-legacy", "http1"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.154"
|
||||
sha2 = "0.11.1"
|
||||
tokio = { version = "1.53.2", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "signal", "process", "sync", "fs"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = "0.3.34"
|
||||
tempfile = "3.27.0"
|
||||
tokio-tungstenite = "0.30.0"
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Supervision du serveur Go (`loki web`) lancé derrière la façade.
|
||||
//!
|
||||
//! Le Go reste propriétaire de presque tout au lot 1 : s'il meurt, la façade
|
||||
//! le relance (attente croissante 1 s → 30 s, remise à 1 s après une minute
|
||||
//! de vie) et répond 502 entre-temps au lieu de tomber avec lui.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
const MIN_DELAY: Duration = Duration::from_secs(1);
|
||||
const MAX_DELAY: Duration = Duration::from_secs(30);
|
||||
const STABLE_AFTER: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Attente avant la relance suivante, connaissant la précédente et la durée
|
||||
/// de vie du processus qui vient de mourir.
|
||||
pub fn next_delay(prev: Duration, lived: Duration) -> Duration {
|
||||
if lived >= STABLE_AFTER {
|
||||
MIN_DELAY
|
||||
} else {
|
||||
(prev * 2).clamp(MIN_DELAY, MAX_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binaire Go à lancer : `LOKI_GO_BIN`, sinon `loki` à côté de l'exécutable
|
||||
/// courant (les deux viennent toujours du même dossier, cf. `which-bin`).
|
||||
pub fn go_bin(env_value: Option<String>, exe: &Path) -> PathBuf {
|
||||
if let Some(p) = env_value.filter(|v| !v.trim().is_empty()) {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
let name = if cfg!(windows) { "loki.exe" } else { "loki" };
|
||||
exe.parent().unwrap_or(Path::new(".")).join(name)
|
||||
}
|
||||
|
||||
pub struct Supervisor {
|
||||
stop: Option<oneshot::Sender<Duration>>,
|
||||
done: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Supervisor {
|
||||
/// Lance `<go_bin> web <port>` en écoute locale et le maintient en vie.
|
||||
pub fn spawn(go_bin: PathBuf, port: u16) -> Supervisor {
|
||||
let (tx, mut rx) = oneshot::channel::<Duration>();
|
||||
let done = tokio::spawn(async move {
|
||||
let mut delay = Duration::ZERO;
|
||||
loop {
|
||||
let started = Instant::now();
|
||||
let mut child = match launch(&go_bin, port) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[loki-rs] lancement de {} impossible : {e}",
|
||||
go_bin.display()
|
||||
);
|
||||
delay = next_delay(delay, Duration::ZERO);
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(delay) => continue,
|
||||
_ = &mut rx => return,
|
||||
}
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
status = child.wait() => {
|
||||
let lived = started.elapsed();
|
||||
delay = next_delay(delay, lived);
|
||||
let status = match status {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
eprintln!(
|
||||
"[loki-rs] serveur Go arrêté ({status}) après {lived:?} — relance dans {delay:?}"
|
||||
);
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(delay) => {}
|
||||
_ = &mut rx => return,
|
||||
}
|
||||
}
|
||||
grace = &mut rx => {
|
||||
stop_child(&mut child, grace.unwrap_or(MIN_DELAY)).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Supervisor {
|
||||
stop: Some(tx),
|
||||
done,
|
||||
}
|
||||
}
|
||||
|
||||
/// Arrête le Go : SIGTERM, puis SIGKILL passé `grace`.
|
||||
pub async fn shutdown(mut self, grace: Duration) {
|
||||
if let Some(tx) = self.stop.take() {
|
||||
let _ = tx.send(grace);
|
||||
}
|
||||
let _ = (&mut self.done).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn launch(go_bin: &Path, port: u16) -> std::io::Result<Child> {
|
||||
Command::new(go_bin)
|
||||
.arg("web")
|
||||
.arg(port.to_string())
|
||||
.env("LOKI_WEB_BIND", "127.0.0.1")
|
||||
// stdin fermé : `loki web` ne doit jamais attendre une réponse au
|
||||
// clavier (résolution de conflit de port).
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
}
|
||||
|
||||
async fn stop_child(child: &mut Child, grace: Duration) {
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = child.id() {
|
||||
// SAFETY: kill(2) sur le PID de notre propre enfant, pas encore récolté.
|
||||
unsafe {
|
||||
libc::kill(pid as libc::pid_t, libc::SIGTERM);
|
||||
}
|
||||
if tokio::time::timeout(grace, child.wait()).await.is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = grace;
|
||||
let _ = child.kill().await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attente_croissante_puis_remise_a_zero() {
|
||||
let s = Duration::from_secs;
|
||||
assert_eq!(next_delay(Duration::ZERO, s(0)), s(1));
|
||||
assert_eq!(next_delay(s(1), s(2)), s(2));
|
||||
assert_eq!(next_delay(s(16), s(2)), s(30));
|
||||
assert_eq!(next_delay(s(30), s(2)), s(30));
|
||||
assert_eq!(next_delay(s(30), s(61)), s(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binaire_go_par_defaut_a_cote_de_l_exe() {
|
||||
let exe = Path::new("/usr/local/bin/loki-rs");
|
||||
let name = if cfg!(windows) { "loki.exe" } else { "loki" };
|
||||
let dir = Path::new("/usr/local/bin");
|
||||
assert_eq!(go_bin(None, exe), dir.join(name));
|
||||
assert_eq!(go_bin(Some(" ".into()), exe), dir.join(name));
|
||||
assert_eq!(
|
||||
go_bin(Some("/x/loki".into()), exe),
|
||||
PathBuf::from("/x/loki")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! loki-rs — façade Rust de Loki (lot 1 de la migration Go → Rust).
|
||||
//!
|
||||
//! - `proxy` : façade HTTP/SSE/WebSocket devant le serveur Go ;
|
||||
//! - `child` : supervision du processus `loki web` (Go) ;
|
||||
//! - `update` : mise à jour des binaires au démarrage du conteneur.
|
||||
|
||||
pub mod child;
|
||||
pub mod proxy;
|
||||
pub mod update;
|
||||
@@ -0,0 +1,151 @@
|
||||
//! loki-rs — façade Rust de Loki.
|
||||
//!
|
||||
//! ```text
|
||||
//! loki-rs serve façade sur LOKI_WEB_PORT (8090), Go sur 127.0.0.1:LOKI_GO_PORT (8091)
|
||||
//! loki-rs self-update met à jour les binaires dans $LOKI_HOME/bin (jamais bloquant)
|
||||
//! loki-rs which-bin dossier d'où prendre loki et loki-rs
|
||||
//! loki-rs version
|
||||
//! ```
|
||||
|
||||
use loki_rs::{child, proxy, update};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let cmd = std::env::args().nth(1).unwrap_or_default();
|
||||
match cmd.as_str() {
|
||||
"serve" => serve(),
|
||||
"self-update" => {
|
||||
let outcome = update::run(&update::UpdateConfig::from_env());
|
||||
eprintln!("[loki-rs update] {}", outcome.message());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
"which-bin" => {
|
||||
let cfg = update::UpdateConfig::from_env();
|
||||
let dir = update::state::select_dir(
|
||||
&cfg.data_bin,
|
||||
&image_dir(),
|
||||
cfg.image_build,
|
||||
cfg.image_abi,
|
||||
);
|
||||
println!("{}", dir.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
"version" | "--version" | "-V" => {
|
||||
let cfg = update::UpdateConfig::from_env();
|
||||
println!(
|
||||
"loki-rs {} (build image {}, ABI {})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
cfg.image_build,
|
||||
cfg.image_abi
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
_ => {
|
||||
eprintln!("usage : loki-rs serve | self-update | which-bin | version");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exe() -> PathBuf {
|
||||
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("loki-rs"))
|
||||
}
|
||||
|
||||
fn image_dir() -> PathBuf {
|
||||
exe()
|
||||
.parent()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
fn env_port(key: &str, default: u16) -> u16 {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn serve() -> ExitCode {
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
eprintln!("[loki-rs] runtime : {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
rt.block_on(async {
|
||||
let web_port = env_port("LOKI_WEB_PORT", 8090);
|
||||
let go_port = env_port("LOKI_GO_PORT", 8091);
|
||||
let go_bin = child::go_bin(std::env::var("LOKI_GO_BIN").ok(), &exe());
|
||||
let backend = SocketAddr::from(([127, 0, 0, 1], go_port));
|
||||
|
||||
eprintln!(
|
||||
"[loki-rs] serveur Go : {} web {go_port} (local)",
|
||||
go_bin.display()
|
||||
);
|
||||
let sup = child::Supervisor::spawn(go_bin, go_port);
|
||||
wait_for(backend, Duration::from_secs(30)).await;
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], web_port));
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("[loki-rs] écoute sur {addr} impossible : {e}");
|
||||
sup.shutdown(Duration::from_secs(10)).await;
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
eprintln!("[loki-rs] façade : http://{addr} → {backend}");
|
||||
let res = proxy::serve(listener, backend, shutdown_signal()).await;
|
||||
eprintln!("[loki-rs] arrêt demandé — arrêt du serveur Go");
|
||||
sup.shutdown(Duration::from_secs(10)).await;
|
||||
match res {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("[loki-rs] {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Attend que le Go accepte les connexions, sans bloquer au-delà de `max` :
|
||||
/// la façade ouvre quand même et répond 502 en attendant.
|
||||
async fn wait_for(addr: SocketAddr, max: Duration) {
|
||||
let t0 = Instant::now();
|
||||
while t0.elapsed() < max {
|
||||
if tokio::net::TcpStream::connect(addr).await.is_ok() {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
eprintln!("[loki-rs] le serveur Go ne répond pas encore — façade ouverte quand même");
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
match signal(SignalKind::terminate()) {
|
||||
Ok(mut term) => {
|
||||
tokio::select! {
|
||||
_ = term.recv() => {}
|
||||
_ = tokio::signal::ctrl_c() => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Façade HTTP : reçoit tout le trafic public et le relaie au serveur Go
|
||||
//! (`loki web` sur 127.0.0.1). Les lots suivants de la migration y brancheront
|
||||
//! les routes réécrites en Rust ; au lot 1, tout est relayé.
|
||||
//!
|
||||
//! Règles, héritées de `web_server.go` :
|
||||
//! - aucun tampon : un flux SSE (chat, `/v1` en streaming) part trame par trame ;
|
||||
//! - aucun délai d'écriture : un flux reste ouvert tant que la page l'écoute ;
|
||||
//! - en-têtes de bout en bout transmis intacts (`Host`, `Authorization`,
|
||||
//! `Origin`, `Sec-Fetch-*`, `X-Forwarded-Proto`…) : le Go s'en sert pour ses
|
||||
//! contrôles d'accès ; seuls les en-têtes hop-by-hop sont retirés ;
|
||||
//! - `X-Loki-Via` entrant effacé : il marque le trafic du tunnel du relais et
|
||||
//! dispense du contrôle d'hôte anti-DNS-rebinding — un client ne doit jamais
|
||||
//! pouvoir le poser lui-même.
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Full, combinators::BoxBody};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Request, Response, StatusCode, Uri};
|
||||
use hyper_util::client::legacy::{Client, connect::HttpConnector};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use std::convert::Infallible;
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub type Body = BoxBody<Bytes, hyper::Error>;
|
||||
|
||||
/// En-tête posé par le tunnel du relais côté Go (`viaTunnelHeader`).
|
||||
pub const VIA_TUNNEL: &str = "x-loki-via";
|
||||
|
||||
const HOP_BY_HOP: [&str; 8] = [
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"proxy-authenticate",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
];
|
||||
|
||||
struct Proxy {
|
||||
client: Client<HttpConnector, Incoming>,
|
||||
backend: SocketAddr,
|
||||
}
|
||||
|
||||
/// Sert la façade sur `listener` jusqu'à ce que `shutdown` se termine.
|
||||
pub async fn serve(
|
||||
listener: TcpListener,
|
||||
backend: SocketAddr,
|
||||
shutdown: impl Future<Output = ()>,
|
||||
) -> std::io::Result<()> {
|
||||
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||
let proxy = Arc::new(Proxy { client, backend });
|
||||
tokio::pin!(shutdown);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut shutdown => return Ok(()),
|
||||
res = listener.accept() => {
|
||||
let stream = match res {
|
||||
Ok((s, _)) => s,
|
||||
Err(e) => {
|
||||
// EMFILE & co : on ne lâche pas l'écoute pour autant.
|
||||
eprintln!("[loki-rs] accept : {e}");
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let _ = stream.set_nodelay(true);
|
||||
let p = proxy.clone();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(move |req| {
|
||||
let p = p.clone();
|
||||
async move { Ok::<_, Infallible>(p.handle(req).await) }
|
||||
});
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.timer(hyper_util::rt::TokioTimer::new())
|
||||
.header_read_timeout(Duration::from_secs(10))
|
||||
.serve_connection(TokioIo::new(stream), svc)
|
||||
.with_upgrades()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
async fn handle(&self, mut req: Request<Incoming>) -> Response<Body> {
|
||||
let upgrade = upgrade_protocol(req.headers());
|
||||
strip_hop_by_hop(req.headers_mut());
|
||||
req.headers_mut().remove(VIA_TUNNEL);
|
||||
let client_upgrade = upgrade.as_ref().map(|proto| {
|
||||
let h = req.headers_mut();
|
||||
h.insert(header::CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
h.insert(header::UPGRADE, proto.clone());
|
||||
hyper::upgrade::on(&mut req)
|
||||
});
|
||||
|
||||
let pq = req
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|p| p.as_str())
|
||||
.unwrap_or("/");
|
||||
let uri: Uri = match format!("http://{}{}", self.backend, pq).parse() {
|
||||
Ok(u) => u,
|
||||
Err(_) => return plain(StatusCode::BAD_REQUEST, "URI invalide"),
|
||||
};
|
||||
*req.uri_mut() = uri;
|
||||
|
||||
let mut resp = match self.client.request(req).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("[loki-rs] backend Go injoignable : {e}");
|
||||
return plain(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"Loki (serveur Go) redémarre — réessaie dans un instant.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(client_upgrade) = client_upgrade
|
||||
&& resp.status() == StatusCode::SWITCHING_PROTOCOLS
|
||||
{
|
||||
let backend_upgrade = hyper::upgrade::on(&mut resp);
|
||||
tokio::spawn(async move {
|
||||
match (client_upgrade.await, backend_upgrade.await) {
|
||||
(Ok(a), Ok(b)) => {
|
||||
let (mut a, mut b) = (TokioIo::new(a), TokioIo::new(b));
|
||||
let _ = tokio::io::copy_bidirectional(&mut a, &mut b).await;
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => eprintln!("[loki-rs] upgrade : {e}"),
|
||||
}
|
||||
});
|
||||
// 101 : Connection/Upgrade font partie de la poignée de main.
|
||||
return resp.map(BodyExt::boxed);
|
||||
}
|
||||
strip_hop_by_hop(resp.headers_mut());
|
||||
resp.map(BodyExt::boxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocole demandé si la requête est une demande d'upgrade (WebSocket).
|
||||
fn upgrade_protocol(h: &HeaderMap) -> Option<HeaderValue> {
|
||||
let wants = h
|
||||
.get_all(header::CONNECTION)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.flat_map(|s| s.split(','))
|
||||
.any(|t| t.trim().eq_ignore_ascii_case("upgrade"));
|
||||
if wants {
|
||||
h.get(header::UPGRADE).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Retire les en-têtes propres à une connexion (RFC 9110 §7.6.1), y compris
|
||||
/// ceux que `Connection` désigne.
|
||||
pub fn strip_hop_by_hop(h: &mut HeaderMap) {
|
||||
let listed: Vec<HeaderName> = h
|
||||
.get_all(header::CONNECTION)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.flat_map(|s| s.split(','))
|
||||
.filter_map(|t| HeaderName::from_bytes(t.trim().as_bytes()).ok())
|
||||
.collect();
|
||||
for n in listed {
|
||||
h.remove(n);
|
||||
}
|
||||
for n in HOP_BY_HOP {
|
||||
h.remove(n);
|
||||
}
|
||||
}
|
||||
|
||||
fn plain(status: StatusCode, msg: &'static str) -> Response<Body> {
|
||||
let mut r = Response::new(
|
||||
Full::new(Bytes::from_static(msg.as_bytes()))
|
||||
.map_err(|n| match n {})
|
||||
.boxed(),
|
||||
);
|
||||
*r.status_mut() = status;
|
||||
r.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/plain; charset=utf-8"),
|
||||
);
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retire_les_hop_by_hop_et_ceux_listes() {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert(
|
||||
"connection",
|
||||
HeaderValue::from_static("keep-alive, x-perso"),
|
||||
);
|
||||
h.insert("keep-alive", HeaderValue::from_static("timeout=5"));
|
||||
h.insert("x-perso", HeaderValue::from_static("1"));
|
||||
h.insert("transfer-encoding", HeaderValue::from_static("chunked"));
|
||||
h.insert("authorization", HeaderValue::from_static("Bearer k"));
|
||||
strip_hop_by_hop(&mut h);
|
||||
assert_eq!(h.len(), 1);
|
||||
assert!(h.contains_key("authorization"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detecte_l_upgrade() {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert(
|
||||
"connection",
|
||||
HeaderValue::from_static("keep-alive, Upgrade"),
|
||||
);
|
||||
h.insert("upgrade", HeaderValue::from_static("websocket"));
|
||||
assert_eq!(
|
||||
upgrade_protocol(&h),
|
||||
Some(HeaderValue::from_static("websocket"))
|
||||
);
|
||||
h.insert("connection", HeaderValue::from_static("keep-alive"));
|
||||
assert_eq!(upgrade_protocol(&h), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Mise à jour des binaires Loki au démarrage du conteneur.
|
||||
//!
|
||||
//! La CI publie à chaque push sur `main` une release GitHub `rolling` :
|
||||
//! `loki-linux`, `loki-rs-linux`, `SHA256SUMS` puis `manifest.json` (en
|
||||
//! dernier). Au boot, [`run`] compare le manifeste au build en service et, si
|
||||
//! besoin, installe les deux binaires dans `<LOKI_HOME>/bin/<build>/` (voir
|
||||
//! [`state`]). Rien ici ne doit bloquer le démarrage : toute erreur se solde
|
||||
//! par [`Outcome::Failed`] et l'ancien état reste en place.
|
||||
//!
|
||||
//! Le transfert HTTPS passe par `curl` (présent dans l'image) : le binaire
|
||||
//! n'embarque ainsi aucune pile TLS.
|
||||
|
||||
pub mod state;
|
||||
pub mod sums;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use state::{Decision, Manifest};
|
||||
|
||||
pub const DEFAULT_URL: &str = "https://github.com/R0m1k3/Loki/releases/download/rolling";
|
||||
pub const IMAGE_ABI_FILE: &str = "/etc/loki/IMAGE_ABI";
|
||||
|
||||
/// Nom de l'asset publié → nom du binaire installé.
|
||||
pub const ASSETS: [(&str, &str); 2] = [
|
||||
("loki-linux", state::GO_BIN),
|
||||
("loki-rs-linux", state::RS_BIN),
|
||||
];
|
||||
|
||||
const TMP_DIR: &str = ".tmp-download";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateConfig {
|
||||
pub enabled: bool,
|
||||
pub base_url: String,
|
||||
pub data_bin: PathBuf,
|
||||
pub image_build: u64,
|
||||
pub image_abi: u32,
|
||||
/// Durée maximale du téléchargement du manifeste et des sommes.
|
||||
pub small_timeout: Duration,
|
||||
/// Durée maximale de chaque binaire (un transfert figé est coupé bien
|
||||
/// avant, cf. `--speed-limit`).
|
||||
pub big_timeout: Duration,
|
||||
}
|
||||
|
||||
impl UpdateConfig {
|
||||
pub fn from_env() -> Self {
|
||||
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.trim().is_empty());
|
||||
let home = env("LOKI_HOME").unwrap_or_else(|| "/data".into());
|
||||
let image_abi = fs::read_to_string(IMAGE_ABI_FILE)
|
||||
.ok()
|
||||
.or_else(|| env("LOKI_IMAGE_ABI"))
|
||||
.and_then(|s| s.trim().parse().ok())
|
||||
.unwrap_or(0);
|
||||
UpdateConfig {
|
||||
enabled: !matches!(
|
||||
env("LOKI_AUTO_UPDATE").as_deref().map(str::trim),
|
||||
Some("0" | "false" | "off" | "no")
|
||||
),
|
||||
base_url: env("LOKI_UPDATE_URL").unwrap_or_else(|| DEFAULT_URL.into()),
|
||||
data_bin: Path::new(&home).join("bin"),
|
||||
image_build: env("LOKI_BUILD")
|
||||
.and_then(|s| s.trim().parse().ok())
|
||||
.unwrap_or(0),
|
||||
image_abi,
|
||||
small_timeout: Duration::from_secs(20),
|
||||
big_timeout: Duration::from_secs(600),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Outcome {
|
||||
Disabled,
|
||||
UpToDate(u64),
|
||||
/// Version publiée incompatible avec l'image : il faut tirer l'image.
|
||||
AbiMismatch {
|
||||
remote_abi: u32,
|
||||
image_abi: u32,
|
||||
},
|
||||
Installed(u64),
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
/// Ligne de journal pour l'entrypoint.
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
Outcome::Disabled => "mise à jour au démarrage désactivée (LOKI_AUTO_UPDATE=0)".into(),
|
||||
Outcome::UpToDate(b) => format!("binaires à jour (build {b})"),
|
||||
Outcome::AbiMismatch {
|
||||
remote_abi,
|
||||
image_abi,
|
||||
} => format!(
|
||||
"nouvelle version disponible mais elle exige une nouvelle image \
|
||||
(ABI {remote_abi}, image {image_abi}) : docker compose pull"
|
||||
),
|
||||
Outcome::Installed(b) => format!("build {b} installé — actif dès ce démarrage"),
|
||||
Outcome::Failed(e) => {
|
||||
format!("mise à jour impossible ({e}) — binaires actuels conservés")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(cfg: &UpdateConfig) -> Outcome {
|
||||
if !cfg.enabled {
|
||||
return Outcome::Disabled;
|
||||
}
|
||||
if let Err(e) = fs::create_dir_all(&cfg.data_bin) {
|
||||
return Outcome::Failed(format!("{}: {e}", cfg.data_bin.display()));
|
||||
}
|
||||
let tmp = cfg.data_bin.join(TMP_DIR);
|
||||
let _ = fs::remove_dir_all(&tmp);
|
||||
let res = fs::create_dir_all(&tmp)
|
||||
.map_err(|e| format!("{}: {e}", tmp.display()))
|
||||
.and_then(|_| run_in(cfg, &tmp));
|
||||
let _ = fs::remove_dir_all(&tmp);
|
||||
res.unwrap_or_else(Outcome::Failed)
|
||||
}
|
||||
|
||||
fn run_in(cfg: &UpdateConfig, tmp: &Path) -> Result<Outcome, String> {
|
||||
let current = state::current(&cfg.data_bin, cfg.image_build, cfg.image_abi)
|
||||
.map(|(m, _)| m.build)
|
||||
.unwrap_or(cfg.image_build);
|
||||
|
||||
let man_path = tmp.join("manifest.json");
|
||||
fetch(&cfg.base_url, "manifest.json", &man_path, cfg.small_timeout)?;
|
||||
let remote = state::read_manifest(&man_path).ok_or("manifest.json illisible")?;
|
||||
|
||||
match state::decide(&remote, current, cfg.image_abi) {
|
||||
Decision::AbiMismatch => {
|
||||
return Ok(Outcome::AbiMismatch {
|
||||
remote_abi: remote.abi,
|
||||
image_abi: cfg.image_abi,
|
||||
});
|
||||
}
|
||||
Decision::UpToDate => {
|
||||
prune(&cfg.data_bin, current);
|
||||
return Ok(Outcome::UpToDate(current));
|
||||
}
|
||||
Decision::Install => {}
|
||||
}
|
||||
|
||||
let sums_path = tmp.join("SHA256SUMS");
|
||||
fetch(&cfg.base_url, "SHA256SUMS", &sums_path, cfg.small_timeout)?;
|
||||
let sums_text = fs::read_to_string(&sums_path).map_err(|e| format!("SHA256SUMS: {e}"))?;
|
||||
let sums = sums::parse(&sums_text);
|
||||
|
||||
let stage = tmp.join("stage");
|
||||
fs::create_dir_all(&stage).map_err(|e| e.to_string())?;
|
||||
for (asset, local) in ASSETS {
|
||||
let want = sums
|
||||
.get(asset)
|
||||
.ok_or(format!("SHA256SUMS sans entrée pour {asset}"))?;
|
||||
let dst = stage.join(local);
|
||||
fetch(&cfg.base_url, asset, &dst, cfg.big_timeout)?;
|
||||
let got = sha256_file(&dst).map_err(|e| format!("{asset}: {e}"))?;
|
||||
if &got != want {
|
||||
return Err(format!("empreinte de {asset} invalide"));
|
||||
}
|
||||
make_executable(&dst).map_err(|e| format!("{asset}: {e}"))?;
|
||||
}
|
||||
write_manifest(&stage.join(state::RELEASE_FILE), &remote)?;
|
||||
|
||||
let dest = cfg.data_bin.join(remote.build.to_string());
|
||||
let _ = fs::remove_dir_all(&dest);
|
||||
fs::rename(&stage, &dest).map_err(|e| format!("{}: {e}", dest.display()))?;
|
||||
|
||||
// Bascule : release.json réécrit en dernier, par renommage atomique.
|
||||
let pointer = cfg.data_bin.join(state::RELEASE_FILE);
|
||||
let pointer_tmp = cfg.data_bin.join("release.json.tmp");
|
||||
write_manifest(&pointer_tmp, &remote)?;
|
||||
fs::rename(&pointer_tmp, &pointer).map_err(|e| format!("{}: {e}", pointer.display()))?;
|
||||
|
||||
prune(&cfg.data_bin, remote.build);
|
||||
Ok(Outcome::Installed(remote.build))
|
||||
}
|
||||
|
||||
/// Télécharge `<base>/<name>` dans `dst` via curl. `--speed-limit` coupe un
|
||||
/// transfert figé (moins de 10 Ko/s pendant 20 s) bien avant `timeout`.
|
||||
fn fetch(base: &str, name: &str, dst: &Path, timeout: Duration) -> Result<(), String> {
|
||||
let url = format!("{}/{}", base.trim_end_matches('/'), name);
|
||||
let out = Command::new("curl")
|
||||
.args([
|
||||
"-fsSL",
|
||||
"--connect-timeout",
|
||||
"10",
|
||||
"--speed-limit",
|
||||
"10240",
|
||||
"--speed-time",
|
||||
"20",
|
||||
])
|
||||
.arg("--max-time")
|
||||
.arg(timeout.as_secs().max(1).to_string())
|
||||
.arg("-o")
|
||||
.arg(dst)
|
||||
.arg(&url)
|
||||
.output()
|
||||
.map_err(|e| format!("curl introuvable : {e}"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
return Err(format!("{name} : {}", err.trim()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sha256_file(path: &Path) -> std::io::Result<String> {
|
||||
let mut f = fs::File::open(path)?;
|
||||
let mut h = Sha256::new();
|
||||
let mut buf = [0u8; 64 * 1024];
|
||||
loop {
|
||||
let n = f.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
h.update(&buf[..n]);
|
||||
}
|
||||
Ok(hex::encode(h.finalize()))
|
||||
}
|
||||
|
||||
fn write_manifest(path: &Path, m: &Manifest) -> Result<(), String> {
|
||||
let text = serde_json::to_string(m).map_err(|e| e.to_string())?;
|
||||
fs::write(path, text).map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn make_executable(path: &Path) -> std::io::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn make_executable(_: &Path) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Supprime les builds autres que `keep` (un seul build est gardé : celui en
|
||||
/// service). Ne touche qu'aux dossiers au nom purement numérique.
|
||||
fn prune(data_bin: &Path, keep: u64) {
|
||||
let Ok(rd) = fs::read_dir(data_bin) else {
|
||||
return;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let name = e.file_name();
|
||||
let Some(name) = name.to_str() else { continue };
|
||||
if let Ok(n) = name.parse::<u64>()
|
||||
&& n != keep
|
||||
&& e.path().is_dir()
|
||||
{
|
||||
let _ = fs::remove_dir_all(e.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//! État d'installation des binaires mis à jour, sous `<LOKI_HOME>/bin` :
|
||||
//!
|
||||
//! ```text
|
||||
//! bin/release.json manifeste du build actif (écrit en DERNIER)
|
||||
//! bin/<build>/loki binaire Go
|
||||
//! bin/<build>/loki-rs binaire Rust
|
||||
//! ```
|
||||
//!
|
||||
//! La bascule d'une version à l'autre se résume à réécrire `release.json` :
|
||||
//! un arrêt brutal pendant une mise à jour laisse l'ancien build intact.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const RELEASE_FILE: &str = "release.json";
|
||||
pub const GO_BIN: &str = "loki";
|
||||
pub const RS_BIN: &str = "loki-rs";
|
||||
|
||||
/// Contenu de `manifest.json` (release) et de `release.json` (installé).
|
||||
///
|
||||
/// `build` est le numéro de run de la CI : il ne fait que croître, c'est lui
|
||||
/// qui ordonne les versions (un commit ne s'ordonne pas). `abi` est l'ABI
|
||||
/// d'image exigée par ces binaires.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
pub build: u64,
|
||||
pub abi: u32,
|
||||
#[serde(default)]
|
||||
pub commit: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Decision {
|
||||
Install,
|
||||
UpToDate,
|
||||
AbiMismatch,
|
||||
}
|
||||
|
||||
pub fn read_manifest(path: &Path) -> Option<Manifest> {
|
||||
let text = std::fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
/// Build installé dans `data_bin` et utilisable avec cette image : même ABI,
|
||||
/// plus récent que les binaires de l'image, et les deux binaires présents.
|
||||
pub fn current(data_bin: &Path, image_build: u64, image_abi: u32) -> Option<(Manifest, PathBuf)> {
|
||||
let m = read_manifest(&data_bin.join(RELEASE_FILE))?;
|
||||
if m.abi != image_abi || m.build <= image_build {
|
||||
return None;
|
||||
}
|
||||
let dir = data_bin.join(m.build.to_string());
|
||||
if !dir.join(GO_BIN).is_file() || !dir.join(RS_BIN).is_file() {
|
||||
return None;
|
||||
}
|
||||
Some((m, dir))
|
||||
}
|
||||
|
||||
/// Dossier dont l'entrypoint doit prendre `loki` et `loki-rs` : toujours les
|
||||
/// deux du même endroit, jamais un Go d'une version avec un Rust d'une autre.
|
||||
pub fn select_dir(data_bin: &Path, image_dir: &Path, image_build: u64, image_abi: u32) -> PathBuf {
|
||||
match current(data_bin, image_build, image_abi) {
|
||||
Some((_, dir)) => dir,
|
||||
None => image_dir.to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Faut-il installer `remote`, sachant que le build en service est `current_build` ?
|
||||
pub fn decide(remote: &Manifest, current_build: u64, image_abi: u32) -> Decision {
|
||||
if remote.abi != image_abi {
|
||||
Decision::AbiMismatch
|
||||
} else if remote.build <= current_build {
|
||||
Decision::UpToDate
|
||||
} else {
|
||||
Decision::Install
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn m(build: u64, abi: u32) -> Manifest {
|
||||
Manifest {
|
||||
build,
|
||||
abi,
|
||||
commit: "abc".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(data_bin: &Path, man: &Manifest, bins: &[&str]) {
|
||||
let dir = data_bin.join(man.build.to_string());
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for b in bins {
|
||||
std::fs::write(dir.join(b), b"x").unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
data_bin.join(RELEASE_FILE),
|
||||
serde_json::to_string(man).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decision() {
|
||||
assert_eq!(decide(&m(10, 2), 5, 1), Decision::AbiMismatch);
|
||||
assert_eq!(decide(&m(5, 1), 5, 1), Decision::UpToDate);
|
||||
assert_eq!(decide(&m(4, 1), 5, 1), Decision::UpToDate);
|
||||
assert_eq!(decide(&m(6, 1), 5, 1), Decision::Install);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_sans_installation() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let img = Path::new("/usr/local/bin");
|
||||
assert_eq!(select_dir(t.path(), img, 3, 1), img);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_installation_valide() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
install(t.path(), &m(7, 1), &[GO_BIN, RS_BIN]);
|
||||
let img = Path::new("/usr/local/bin");
|
||||
assert_eq!(select_dir(t.path(), img, 3, 1), t.path().join("7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_refuse_abi_differente() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
install(t.path(), &m(7, 2), &[GO_BIN, RS_BIN]);
|
||||
let img = Path::new("/img");
|
||||
assert_eq!(select_dir(t.path(), img, 3, 1), img);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_refuse_plus_ancien_que_l_image() {
|
||||
// Image tirée à la main plus récente que /data/bin : l'image gagne.
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
install(t.path(), &m(7, 1), &[GO_BIN, RS_BIN]);
|
||||
let img = Path::new("/img");
|
||||
assert_eq!(select_dir(t.path(), img, 7, 1), img);
|
||||
assert_eq!(select_dir(t.path(), img, 9, 1), img);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_refuse_binaire_manquant() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
install(t.path(), &m(7, 1), &[GO_BIN]);
|
||||
let img = Path::new("/img");
|
||||
assert_eq!(select_dir(t.path(), img, 3, 1), img);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Lecture du fichier SHA256SUMS publié avec chaque release (format
|
||||
//! `sha256sum` : "<hex> <nom>", ou "<hex> *<nom>" en mode binaire).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Associe chaque nom de fichier à son empreinte SHA-256 en hexadécimal
|
||||
/// minuscule. Les lignes vides ou mal formées sont ignorées : une entrée
|
||||
/// manquante fera échouer la vérification du fichier concerné, pas la lecture.
|
||||
pub fn parse(text: &str) -> HashMap<String, String> {
|
||||
let mut out = HashMap::new();
|
||||
for line in text.lines() {
|
||||
let mut it = line.split_whitespace();
|
||||
let (Some(hash), Some(name), None) = (it.next(), it.next(), it.next()) else {
|
||||
continue;
|
||||
};
|
||||
if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
continue;
|
||||
}
|
||||
out.insert(
|
||||
name.trim_start_matches('*').to_string(),
|
||||
hash.to_ascii_lowercase(),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
#[test]
|
||||
fn lit_les_deux_formats() {
|
||||
let up = A.to_ascii_uppercase();
|
||||
let text = format!("{A} loki-linux\n{up} *loki-rs-linux\n");
|
||||
let m = parse(&text);
|
||||
assert_eq!(m.get("loki-linux").map(String::as_str), Some(A));
|
||||
assert_eq!(m.get("loki-rs-linux").map(String::as_str), Some(A));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_les_lignes_cassees() {
|
||||
let text = format!("\nzz loki-linux\n{A}\n{A} a b\n{A} ok\n");
|
||||
let m = parse(&text);
|
||||
assert_eq!(m.len(), 1);
|
||||
assert!(m.contains_key("ok"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Serveur HTTP minimal pour les tests : sert des fichiers en mémoire et
|
||||
//! journalise les chemins demandés.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::Full;
|
||||
use hyper::{Request, Response, StatusCode, body::Incoming, service::service_fn};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct FileServer {
|
||||
pub addr: SocketAddr,
|
||||
pub hits: Arc<Mutex<Vec<String>>>,
|
||||
_rt: tokio::runtime::Runtime,
|
||||
}
|
||||
|
||||
impl FileServer {
|
||||
pub fn url(&self) -> String {
|
||||
format!("http://{}/rolling", self.addr)
|
||||
}
|
||||
pub fn hits(&self) -> Vec<String> {
|
||||
self.hits.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serve_files(files: HashMap<String, Vec<u8>>) -> FileServer {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let files = Arc::new(files);
|
||||
let hits = Arc::new(Mutex::new(Vec::new()));
|
||||
let listener = rt
|
||||
.block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
|
||||
.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let h = hits.clone();
|
||||
rt.spawn(async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
return;
|
||||
};
|
||||
let files = files.clone();
|
||||
let h = h.clone();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(move |req: Request<Incoming>| {
|
||||
let files = files.clone();
|
||||
let h = h.clone();
|
||||
async move {
|
||||
let path = req.uri().path().trim_start_matches("/rolling/").to_string();
|
||||
h.lock().unwrap().push(path.clone());
|
||||
let resp = match files.get(&path) {
|
||||
Some(b) => Response::new(Full::new(Bytes::from(b.clone()))),
|
||||
None => {
|
||||
let mut r = Response::new(Full::new(Bytes::from_static(b"404")));
|
||||
*r.status_mut() = StatusCode::NOT_FOUND;
|
||||
r
|
||||
}
|
||||
};
|
||||
Ok::<_, std::convert::Infallible>(resp)
|
||||
}
|
||||
});
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(stream), svc)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
FileServer {
|
||||
addr,
|
||||
hits,
|
||||
_rt: rt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use bytes::Bytes;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use http_body_util::{BodyExt, Empty, Full, StreamBody, combinators::BoxBody};
|
||||
use hyper::body::{Frame, Incoming};
|
||||
use hyper::{Request, Response, StatusCode, service::service_fn};
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
type B = BoxBody<Bytes, Infallible>;
|
||||
|
||||
/// Backend factice : /echo renvoie les en-têtes reçus, /sse émet un événement,
|
||||
/// attend 500 ms, puis un second ; /post renvoie le corps reçu.
|
||||
async fn backend() -> SocketAddr {
|
||||
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let (s, _) = l.accept().await.unwrap();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(|req: Request<Incoming>| async move {
|
||||
let resp: Response<B> = match req.uri().path() {
|
||||
"/echo" => {
|
||||
let mut lines: Vec<String> = req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}: {}", k, v.to_str().unwrap()))
|
||||
.collect();
|
||||
lines.push(format!("uri: {}", req.uri()));
|
||||
Response::new(Full::new(Bytes::from(lines.join("\n"))).boxed())
|
||||
}
|
||||
"/post" => {
|
||||
let body = req.into_body().collect().await.unwrap().to_bytes();
|
||||
Response::new(Full::new(body).boxed())
|
||||
}
|
||||
"/sse" => {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(4);
|
||||
tokio::spawn(async move {
|
||||
tx.send(Bytes::from_static(b"data: un\n\n")).await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
let _ = tx.send(Bytes::from_static(b"data: deux\n\n")).await;
|
||||
});
|
||||
let stream = futures_util::stream::unfold(rx, |mut rx| async move {
|
||||
rx.recv()
|
||||
.await
|
||||
.map(|b| (Ok::<_, Infallible>(Frame::data(b)), rx))
|
||||
});
|
||||
let mut r = Response::new(BodyExt::boxed(StreamBody::new(stream)));
|
||||
r.headers_mut()
|
||||
.insert("content-type", "text/event-stream".parse().unwrap());
|
||||
r
|
||||
}
|
||||
_ => Response::new(Empty::new().boxed()),
|
||||
};
|
||||
Ok::<_, Infallible>(resp)
|
||||
});
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(s), svc)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
async fn ws_backend() -> SocketAddr {
|
||||
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
tokio::spawn(async move {
|
||||
while let Ok((s, _)) = l.accept().await {
|
||||
tokio::spawn(async move {
|
||||
let mut ws = tokio_tungstenite::accept_async(s).await.unwrap();
|
||||
while let Some(Ok(m)) = ws.next().await {
|
||||
if m.is_text() {
|
||||
ws.send(m).await.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// Lance la façade devant `backend`, rend son adresse.
|
||||
async fn facade(backend: SocketAddr) -> (SocketAddr, oneshot::Sender<()>) {
|
||||
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(loki_rs::proxy::serve(l, backend, async move {
|
||||
let _ = rx.await;
|
||||
}));
|
||||
(addr, tx)
|
||||
}
|
||||
|
||||
fn client() -> Client<hyper_util::client::legacy::connect::HttpConnector, Full<Bytes>> {
|
||||
Client::builder(TokioExecutor::new()).build_http()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transmet_les_en_tetes_et_efface_x_loki_via() {
|
||||
let (f, _stop) = facade(backend().await).await;
|
||||
let req = Request::get(format!("http://{f}/echo?a=1"))
|
||||
.header("host", "loki.exemple.fr")
|
||||
.header("authorization", "Bearer secret")
|
||||
.header("origin", "https://loki.exemple.fr")
|
||||
.header("x-forwarded-proto", "https")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("x-loki-via", "tunnel")
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
let resp = client().request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = String::from_utf8(
|
||||
resp.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
for want in [
|
||||
"host: loki.exemple.fr",
|
||||
"authorization: Bearer secret",
|
||||
"origin: https://loki.exemple.fr",
|
||||
"x-forwarded-proto: https",
|
||||
"sec-fetch-site: same-origin",
|
||||
"uri: /echo?a=1",
|
||||
] {
|
||||
assert!(
|
||||
body.lines().any(|l| l == want),
|
||||
"manque « {want} » dans :\n{body}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!body.contains("x-loki-via"),
|
||||
"X-Loki-Via a traversé :\n{body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn relaie_le_corps() {
|
||||
let (f, _stop) = facade(backend().await).await;
|
||||
let req = Request::post(format!("http://{f}/post"))
|
||||
.body(Full::new(Bytes::from_static(b"{\"x\":1}")))
|
||||
.unwrap();
|
||||
let resp = client().request(req).await.unwrap();
|
||||
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&body[..], b"{\"x\":1}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sse_sans_tampon() {
|
||||
let (f, _stop) = facade(backend().await).await;
|
||||
let t0 = Instant::now();
|
||||
let req = Request::get(format!("http://{f}/sse"))
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
let mut body = client().request(req).await.unwrap().into_body();
|
||||
let first = body.frame().await.unwrap().unwrap().into_data().unwrap();
|
||||
let elapsed = t0.elapsed();
|
||||
assert_eq!(&first[..], b"data: un\n\n");
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(300),
|
||||
"premier événement reçu après {elapsed:?}"
|
||||
);
|
||||
let second = body.frame().await.unwrap().unwrap().into_data().unwrap();
|
||||
assert_eq!(&second[..], b"data: deux\n\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_relaye() {
|
||||
let (f, _stop) = facade(ws_backend().await).await;
|
||||
let (mut ws, resp) = tokio_tungstenite::connect_async(format!("ws://{f}/api/node/ws"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SWITCHING_PROTOCOLS);
|
||||
ws.send(Message::text("bonjour")).await.unwrap();
|
||||
let got = tokio::time::timeout(Duration::from_secs(5), ws.next())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(got.into_text().unwrap().as_str(), "bonjour");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backend_absent_donne_502() {
|
||||
// Port réservé puis libéré : personne n'y écoute.
|
||||
let dead = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap();
|
||||
let (f, _stop) = facade(dead).await;
|
||||
let req = Request::get(format!("http://{f}/"))
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
let resp = client().request(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
mod common;
|
||||
|
||||
use loki_rs::update::{self, Outcome, UpdateConfig, sha256_file, state};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
const GO: &[u8] = b"binaire go";
|
||||
const RS: &[u8] = b"binaire rust";
|
||||
|
||||
fn sha(b: &[u8]) -> String {
|
||||
let t = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(t.path(), b).unwrap();
|
||||
sha256_file(t.path()).unwrap()
|
||||
}
|
||||
|
||||
fn release(
|
||||
build: u64,
|
||||
abi: u32,
|
||||
go: &[u8],
|
||||
rs: &[u8],
|
||||
sums_go: Option<&str>,
|
||||
) -> HashMap<String, Vec<u8>> {
|
||||
let sums = format!(
|
||||
"{} loki-linux\n{} loki-rs-linux\n",
|
||||
sums_go.map(str::to_string).unwrap_or_else(|| sha(go)),
|
||||
sha(rs)
|
||||
);
|
||||
HashMap::from([
|
||||
(
|
||||
"manifest.json".into(),
|
||||
format!(r#"{{"build":{build},"abi":{abi},"commit":"c{build}"}}"#).into_bytes(),
|
||||
),
|
||||
("SHA256SUMS".into(), sums.into_bytes()),
|
||||
("loki-linux".into(), go.to_vec()),
|
||||
("loki-rs-linux".into(), rs.to_vec()),
|
||||
])
|
||||
}
|
||||
|
||||
fn cfg(url: String, data_bin: &Path) -> UpdateConfig {
|
||||
UpdateConfig {
|
||||
enabled: true,
|
||||
base_url: url,
|
||||
data_bin: data_bin.to_path_buf(),
|
||||
image_build: 3,
|
||||
image_abi: 1,
|
||||
small_timeout: Duration::from_secs(10),
|
||||
big_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installe_puis_reconnait_a_jour() {
|
||||
let srv = common::serve_files(release(7, 1, GO, RS, None));
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let c = cfg(srv.url(), t.path());
|
||||
|
||||
assert_eq!(update::run(&c), Outcome::Installed(7));
|
||||
let dir = state::select_dir(t.path(), Path::new("/img"), 3, 1);
|
||||
assert_eq!(dir, t.path().join("7"));
|
||||
assert_eq!(std::fs::read(dir.join("loki")).unwrap(), GO);
|
||||
assert_eq!(std::fs::read(dir.join("loki-rs")).unwrap(), RS);
|
||||
assert!(!t.path().join(".tmp-download").exists());
|
||||
|
||||
assert_eq!(update::run(&c), Outcome::UpToDate(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nouveau_build_remplace_et_nettoie_l_ancien() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let srv = common::serve_files(release(7, 1, GO, RS, None));
|
||||
assert_eq!(
|
||||
update::run(&cfg(srv.url(), t.path())),
|
||||
Outcome::Installed(7)
|
||||
);
|
||||
let srv2 = common::serve_files(release(9, 1, b"go 9", b"rs 9", None));
|
||||
assert_eq!(
|
||||
update::run(&cfg(srv2.url(), t.path())),
|
||||
Outcome::Installed(9)
|
||||
);
|
||||
assert!(!t.path().join("7").exists());
|
||||
assert_eq!(
|
||||
state::select_dir(t.path(), Path::new("/img"), 3, 1),
|
||||
t.path().join("9")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empreinte_fausse_ne_change_rien() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let srv = common::serve_files(release(7, 1, GO, RS, None));
|
||||
assert_eq!(
|
||||
update::run(&cfg(srv.url(), t.path())),
|
||||
Outcome::Installed(7)
|
||||
);
|
||||
|
||||
let bad = "0".repeat(64);
|
||||
let srv2 = common::serve_files(release(9, 1, b"go 9", b"rs 9", Some(&bad)));
|
||||
assert!(matches!(
|
||||
update::run(&cfg(srv2.url(), t.path())),
|
||||
Outcome::Failed(_)
|
||||
));
|
||||
assert_eq!(
|
||||
state::select_dir(t.path(), Path::new("/img"), 3, 1),
|
||||
t.path().join("7")
|
||||
);
|
||||
assert!(!t.path().join("9").exists());
|
||||
assert!(!t.path().join(".tmp-download").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_differente_ne_telecharge_que_le_manifeste() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let srv = common::serve_files(release(7, 2, GO, RS, None));
|
||||
assert_eq!(
|
||||
update::run(&cfg(srv.url(), t.path())),
|
||||
Outcome::AbiMismatch {
|
||||
remote_abi: 2,
|
||||
image_abi: 1
|
||||
}
|
||||
);
|
||||
assert_eq!(srv.hits(), vec!["manifest.json".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_plus_recente_que_la_release() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let srv = common::serve_files(release(3, 1, GO, RS, None));
|
||||
assert_eq!(update::run(&cfg(srv.url(), t.path())), Outcome::UpToDate(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serveur_absent() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let mut c = cfg("http://127.0.0.1:9/rolling".into(), t.path());
|
||||
c.small_timeout = Duration::from_secs(3);
|
||||
assert!(matches!(update::run(&c), Outcome::Failed(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desactive_sans_requete() {
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let srv = common::serve_files(release(7, 1, GO, RS, None));
|
||||
let mut c = cfg(srv.url(), t.path());
|
||||
c.enabled = false;
|
||||
assert_eq!(update::run(&c), Outcome::Disabled);
|
||||
assert!(srv.hits().is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user