Fixes and improvements

This commit is contained in:
imengying committed 2026-08-11 22:50:34 +08:00
1 parent 1c9540c13c
commit f24efd1401
33 files changed
+2618 -1093

No files matched your search

+180 -237
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "base64"
version = "0.22.1"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bitflags"
@@ -16,34 +16,29 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "block-buffer"
version = "0.10.4"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"generic-array",
"hybrid-array",
]
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
version = "1.4.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "nodeflare-agent"
version = "0.0.1"
dependencies = [
"serde",
"serde_json",
"sha2",
"sysinfo",
"ureq",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -51,47 +46,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "core-foundation"
version = "0.10.1"
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "cookie"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
dependencies = [
"core-foundation-sys",
"libc",
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"generic-array",
"typenum",
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.10.7"
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[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",
]
@@ -107,41 +127,25 @@ dependencies = [
]
[[package]]
name = "errno"
version = "0.3.14"
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"libc",
"windows-sys 0.61.2",
"litrs",
]
[[package]]
name = "fastrand"
version = "2.5.0"
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
[[package]]
name = "form_urlencoded"
@@ -152,16 +156,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -174,14 +168,34 @@ dependencies = [
]
[[package]]
name = "getrandom"
version = "0.4.3"
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
@@ -287,6 +301,16 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -299,18 +323,18 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "log"
version = "0.4.33"
@@ -324,20 +348,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
name = "nodeflare-agent"
version = "0.0.1"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
"serde",
"serde_json",
"sha2",
"sysinfo",
"ureq",
"url",
]
[[package]]
@@ -349,6 +368,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
@@ -374,61 +399,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl"
version = "0.10.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -438,6 +414,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -456,12 +438,6 @@ 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 = "ring"
version = "0.17.14"
@@ -470,23 +446,10 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"getrandom",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[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 0.61.2",
"windows-sys",
]
[[package]]
@@ -524,38 +487,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.229"
@@ -601,9 +532,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.9"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -682,16 +613,33 @@ dependencies = [
]
[[package]]
name = "tempfile"
version = "3.27.0"
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
@@ -724,20 +672,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
dependencies = [
"base64",
"cookie_store",
"log",
"native-tls",
"once_cell",
"percent-encoding",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"url",
"webpki-roots 0.26.11",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
@@ -752,18 +713,18 @@ dependencies = [
"serde",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -776,15 +737,6 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.9",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
@@ -926,15 +878,6 @@ dependencies = [
"windows-targets",
]
[[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 = "windows-targets"
version = "0.52.6"
+3 -5
View File
@@ -9,14 +9,12 @@ repository = "https://github.com/imengying/NodeFlare"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
[target.'cfg(target_os = "linux")'.dependencies]
ureq = { version = "2.12", default-features = false, features = ["json", "tls"] }
sha2 = "0.11"
ureq = { version = "3.4", default-features = false, features = ["json", "rustls"] }
url = "2.5"
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
sysinfo = { version = "0.39.6", default-features = false, features = ["disk", "network", "system"] }
ureq = { version = "2.12", default-features = false, features = ["json", "native-tls"] }
[profile.release]
codegen-units = 1
+5 -1
View File
@@ -38,7 +38,11 @@ install_agent() {
collect_interval=$(arg --collect-interval "$@" || printf '5')
worker_url=${worker_url%/}
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid install argument" >&2; exit 1; }
case "$worker_url" in http://?*|https://?*) ;; *) echo "Worker URL must use HTTP or HTTPS" >&2; exit 1 ;; esac
case "$worker_url" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
esac
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
+5 -1
View File
@@ -46,7 +46,11 @@ interval=$(arg --interval "$@" || printf '60')
collect_interval=$(arg --collect-interval "$@" || printf '5')
worker_url=${worker_url%/}
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid argument" >&2; exit 1; }
case "$worker_url" in http://?*|https://?*) ;; *) echo "Worker URL must use HTTP or HTTPS" >&2; exit 1 ;; esac
case "$worker_url" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
esac
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
+8 -3
View File
@@ -21,17 +21,22 @@ function Assert-Safe([string]$Name, [string]$Value) {
function Assert-WorkerUrl([string]$Value) {
$Parsed = $null
$SecureScheme = $false
if ([Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Parsed)) {
$SecureScheme = $Parsed.Scheme -eq "https" -or (
$Parsed.Scheme -eq "http" -and $Parsed.Host -in @("localhost", "127.0.0.1", "::1")
)
}
if (
$Value.Length -gt 2048 -or
$Value -match "\s" -or
$Value.Contains("'") -or
-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Parsed) -or
$Parsed.Scheme -notin @("http", "https") -or
-not $SecureScheme -or
-not [string]::IsNullOrEmpty($Parsed.UserInfo) -or
-not [string]::IsNullOrEmpty($Parsed.Query) -or
-not [string]::IsNullOrEmpty($Parsed.Fragment)
) {
throw "Url must be an absolute HTTP or HTTPS URL"
throw "Url must use HTTPS; HTTP is only allowed for loopback development"
}
}
+216 -106
View File
@@ -2,9 +2,7 @@ use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::io::{self, Read, Write};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use std::net::UdpSocket;
use std::net::{TcpStream, ToSocketAddrs};
use std::net::{IpAddr, SocketAddr, TcpStream, ToSocketAddrs};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
@@ -50,10 +48,12 @@ struct RuntimeConfig {
report_interval: u64,
collect_interval: u64,
network_interface: String,
auto_update: bool,
latency_tasks: Vec<LatencyTask>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
struct LatencyTask {
id: String,
name: String,
@@ -70,16 +70,13 @@ struct LatencyResult {
packet_loss: f64,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RemoteConfig {
#[serde(default)]
report_interval: u64,
#[serde(default)]
collect_interval: u64,
#[serde(default)]
network_interface: String,
auto_update: i64,
#[serde(default)]
latency_tasks: Vec<LatencyTask>,
}
@@ -96,8 +93,15 @@ struct GithubReleaseAsset {
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ReportResponse {
success: bool,
config: RemoteConfig,
}
struct SubmitResult {
config: Option<RemoteConfig>,
config_hash: String,
}
#[derive(Debug, Serialize)]
@@ -120,7 +124,7 @@ struct DiskMetric {
utilization: f64,
}
#[derive(Debug, Default, Clone, Serialize)]
#[derive(Debug, Clone, Serialize)]
struct GpuMetric {
model: String,
usage: f64,
@@ -155,8 +159,6 @@ struct Report {
kernel: String,
arch: String,
virtualization: String,
ipv4: String,
ipv6: String,
gpu_usage: f64,
gpu_model: String,
agent_version: String,
@@ -392,17 +394,6 @@ fn cpu_model() -> String {
.unwrap_or_default()
}
#[cfg(target_os = "linux")]
fn ip_address(family: &str) -> String {
command("ip", &["-o", family, "addr", "show", "scope", "global"])
.lines()
.next()
.and_then(|line| line.split_whitespace().find(|value| value.contains('/')))
.and_then(|value| value.split('/').next())
.unwrap_or("")
.to_string()
}
fn valid_probe_host(host: &str) -> bool {
if host.is_empty() || host.len() > 50 || host.starts_with('.') || host.ends_with('.') {
return false;
@@ -413,24 +404,76 @@ fn valid_probe_host(host: &str) -> bool {
.iter()
.all(|label| !label.is_empty() && label.chars().all(|c| c.is_ascii_digit()));
if ipv4_like {
return labels.iter().all(|label| label.parse::<u8>().is_ok());
return host
.parse()
.is_ok_and(|address| is_public_probe_ip(IpAddr::V4(address)));
}
let lower = host.to_ascii_lowercase();
if labels.len() < 2
|| ["local", "localhost", "internal", "lan", "localdomain"]
.iter()
.any(|suffix| lower == *suffix || lower.ends_with(&format!(".{suffix}")))
|| lower == "home.arpa"
|| lower.ends_with(".home.arpa")
{
return false;
}
labels.iter().all(|label| {
!label.is_empty()
&& label.chars().all(|character| {
character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
})
&& label
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '-')
&& label
.chars()
.next()
.is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
.is_some_and(|character| character.is_ascii_alphanumeric())
&& label
.chars()
.last()
.is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
.is_some_and(|character| character.is_ascii_alphanumeric())
})
}
fn is_public_probe_ip(address: IpAddr) -> bool {
match address {
IpAddr::V4(address) => {
let [a, b, c, _] = address.octets();
!(a == 0
|| a == 10
|| a == 127
|| (a == 100 && (64..=127).contains(&b))
|| (a == 169 && b == 254)
|| (a == 172 && (16..=31).contains(&b))
|| (a == 192 && b == 0 && c == 0)
|| (a == 192 && b == 0 && c == 2)
|| (a == 192 && b == 168)
|| (a == 198 && (b == 18 || b == 19))
|| (a == 198 && b == 51 && c == 100)
|| (a == 203 && b == 0 && c == 113)
|| a >= 224)
}
IpAddr::V6(address) => {
if let Some(mapped) = address.to_ipv4_mapped() {
return is_public_probe_ip(IpAddr::V4(mapped));
}
address.segments()[0] & 0xe000 == 0x2000
}
}
}
fn resolve_public_probe_address(host: &str, port: u16) -> Option<SocketAddr> {
let addresses = (host, port)
.to_socket_addrs()
.ok()?
.filter(|address| is_public_probe_ip(address.ip()))
.collect::<Vec<_>>();
addresses
.iter()
.find(|address| address.is_ipv4())
.or_else(|| addresses.first())
.copied()
}
fn parse_probe_target(value: &str) -> Option<(String, u16)> {
let raw = value.trim();
if raw.is_empty()
@@ -466,23 +509,17 @@ fn tcp_latency_probe(target: &str) -> (f64, f64) {
let Some((host, port)) = parse_probe_target(target) else {
return (-1.0, 100.0);
};
let Ok(addresses) = (host.as_str(), port).to_socket_addrs() else {
return (-1.0, 100.0);
};
let addresses = addresses.collect::<Vec<_>>();
let Some(address) = addresses
.iter()
.find(|address| address.is_ipv4())
.or_else(|| addresses.first())
.copied()
else {
let Some(address) = resolve_public_probe_address(&host, port) else {
return (-1.0, 100.0);
};
tcp_latency_probe_address(&address)
}
fn tcp_latency_probe_address(address: &SocketAddr) -> (f64, f64) {
let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS);
for _ in 0..PROBE_ATTEMPTS {
let started = Instant::now();
if TcpStream::connect_timeout(&address, PROBE_TIMEOUT).is_ok() {
if TcpStream::connect_timeout(address, PROBE_TIMEOUT).is_ok() {
latencies.push(started.elapsed().as_secs_f64() * 1000.0);
}
}
@@ -511,19 +548,26 @@ fn ping_latency(output: &str) -> Option<f64> {
fn icmp_latency_probe(target: &str) -> (f64, f64) {
let host = target.trim();
if host.contains(':') || !valid_probe_host(host) {
if host.contains(':') {
return (-1.0, 100.0);
}
let Some((host, _)) = parse_probe_target(host) else {
return (-1.0, 100.0);
};
let Some(address) = resolve_public_probe_address(&host, 443) else {
return (-1.0, 100.0);
};
let destination = address.ip().to_string();
let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS);
for _ in 0..PROBE_ATTEMPTS {
let started = Instant::now();
let mut ping = Command::new("ping");
#[cfg(target_os = "linux")]
ping.args(["-n", "-c", "1", "-W", "1", host]);
ping.args(["-n", "-c", "1", "-W", "1", destination.as_str()]);
#[cfg(target_os = "macos")]
ping.args(["-n", "-c", "1", "-W", "1000", host]);
ping.args(["-n", "-c", "1", "-W", "1000", destination.as_str()]);
#[cfg(target_os = "windows")]
ping.args(["-n", "1", "-w", "1000", host]);
ping.args(["-n", "1", "-w", "1000", destination.as_str()]);
let output = ping.output();
if let Ok(output) = output {
if output.status.success() {
@@ -611,17 +655,6 @@ fn gpu_info() -> Vec<GpuMetric> {
.collect()
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn local_ip(bind: &str, destination: &str) -> String {
UdpSocket::bind(bind)
.and_then(|socket| {
socket.connect(destination)?;
socket.local_addr()
})
.map(|address| address.ip().to_string())
.unwrap_or_default()
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn u64_to_i64(value: u64) -> i64 {
value.min(i64::MAX as u64) as i64
@@ -720,8 +753,6 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
kernel: command("uname", &["-r"]),
arch: env::consts::ARCH.to_string(),
virtualization: command("systemd-detect-virt", &[]),
ipv4: ip_address("-4"),
ipv6: ip_address("-6"),
gpu_usage,
gpu_model,
agent_version: VERSION.to_string(),
@@ -840,8 +871,6 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
kernel: System::kernel_version().unwrap_or_default(),
arch: env::consts::ARCH.to_string(),
virtualization: String::new(),
ipv4: local_ip("0.0.0.0:0", "1.1.1.1:80"),
ipv6: local_ip("[::]:0", "[2606:4700:4700::1111]:80"),
gpu_usage,
gpu_model,
agent_version: VERSION.to_string(),
@@ -886,7 +915,9 @@ fn runtime_config() -> Result<RuntimeConfig> {
return Err("SERVER_ID or AGENT_TOKEN is invalid".into());
}
if !valid_worker_url(&worker_url) {
return Err("WORKER_URL must be an http(s) URL without whitespace".into());
return Err(
"WORKER_URL must use HTTPS; HTTP is only allowed for loopback development".into(),
);
}
Ok(RuntimeConfig {
server_id,
@@ -895,6 +926,7 @@ fn runtime_config() -> Result<RuntimeConfig> {
report_interval: interval,
collect_interval,
network_interface: env::var("NETWORK_INTERFACE").unwrap_or_default(),
auto_update: true,
latency_tasks: Vec::new(),
})
}
@@ -903,17 +935,38 @@ fn submit(
agent: &ureq::Agent,
config: &RuntimeConfig,
reports: &[Report],
) -> Result<Option<RemoteConfig>> {
config_hash: &str,
) -> Result<SubmitResult> {
let batch = ReportBatch {
server_id: &config.server_id,
samples: reports,
};
let response = agent
let mut response = agent
.post(&format!("{}/api/agent/report", config.worker_url))
.set("Authorization", &format!("Bearer {}", config.token))
.set("User-Agent", &format!("nodeflare-agent/{VERSION}"))
.header("Authorization", format!("Bearer {}", config.token))
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
.header("X-Agent-Config-Schema", "1")
.header("X-Agent-Config-Sha256", config_hash)
.send_json(batch)?;
Ok(response.into_json::<ReportResponse>()?.config)
let next_hash = response
.headers()
.get("X-Agent-Config-Sha256")
.and_then(|value| value.to_str().ok())
.unwrap_or(config_hash)
.to_string();
let remote = if response.status().as_u16() == 204 {
None
} else {
let payload = response.body_mut().read_json::<ReportResponse>()?;
if !payload.success {
return Err("Worker rejected the report".into());
}
Some(payload.config)
};
Ok(SubmitResult {
config: remote,
config_hash: next_hash,
})
}
fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool {
@@ -926,6 +979,7 @@ fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool {
config
.network_interface
.clone_from(&remote.network_interface);
config.auto_update = remote.auto_update == 1;
let tasks = sanitize_latency_tasks(&remote.latency_tasks);
let changed = config.latency_tasks != tasks;
config.latency_tasks = tasks;
@@ -954,15 +1008,32 @@ fn sanitize_latency_tasks(tasks: &[LatencyTask]) -> Vec<LatencyTask> {
fn valid_worker_url(value: &str) -> bool {
let value = value.trim_end_matches('/');
let authority = value
.strip_prefix("https://")
.or_else(|| value.strip_prefix("http://"))
.and_then(|remainder| remainder.split('/').next());
value.len() <= 2048
&& authority.is_some_and(|authority| !authority.is_empty() && !authority.contains('@'))
&& !value
.chars()
.any(|character| character.is_whitespace() || matches!(character, '?' | '#'))
if value.is_empty() || value.len() > 2048 || value.chars().any(char::is_whitespace) {
return false;
}
let Ok(parsed) = url::Url::parse(value) else {
return false;
};
if parsed.host().is_none()
|| !parsed.username().is_empty()
|| parsed.password().is_some()
|| parsed.query().is_some()
|| parsed.fragment().is_some()
{
return false;
}
if parsed.scheme() == "https" {
return true;
}
if parsed.scheme() != "http" {
return false;
}
match parsed.host() {
Some(url::Host::Domain(host)) => host.eq_ignore_ascii_case("localhost"),
Some(url::Host::Ipv4(address)) => address == std::net::Ipv4Addr::LOCALHOST,
Some(url::Host::Ipv6(address)) => address.is_loopback(),
None => false,
}
}
fn report_retry_delay(failures: u32, report_interval: u64) -> Duration {
@@ -1037,7 +1108,11 @@ fn sha256_file(path: &Path) -> Result<String> {
}
digest.update(&buffer[..read]);
}
Ok(format!("{:x}", digest.finalize()))
Ok(digest
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn parse_checksum(contents: &str, artifact: &str) -> Option<String> {
@@ -1058,6 +1133,7 @@ fn release_checksum(agent: &ureq::Agent, url: &str, artifact: &str) -> Result<St
agent
.get(url)
.call()?
.into_body()
.into_reader()
.take(1024 * 1024)
.read_to_string(&mut contents)?;
@@ -1075,7 +1151,7 @@ fn download_agent(
let Ok(response) = agent.get(url).call() else {
return Ok(false);
};
let response = response.into_reader();
let response = response.into_body().into_reader();
let mut file = fs::File::create(destination)?;
let copied = io::copy(&mut response.take(MAX_AGENT_BINARY_BYTES + 1), &mut file)?;
file.flush()?;
@@ -1105,12 +1181,12 @@ fn download_agent(
}
fn update(agent: &ureq::Agent) -> Result<bool> {
let release = agent
let mut response = agent
.get(LATEST_RELEASE_API)
.set("Accept", "application/vnd.github+json")
.set("User-Agent", &format!("nodeflare-agent/{VERSION}"))
.call()?
.into_json::<GithubRelease>()?;
.header("Accept", "application/vnd.github+json")
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
.call()?;
let release = response.body_mut().read_json::<GithubRelease>()?;
let Some(remote_version) = version_triplet(&release.tag_name) else {
return Ok(false);
};
@@ -1199,9 +1275,10 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
fn run(once: bool, print_only: bool) -> Result<()> {
let mut config = runtime_config()?;
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(20))
.build();
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_global(Some(Duration::from_secs(20)))
.build()
.into();
let mut next_report = Instant::now();
let mut next_collect = Instant::now();
let mut next_latency: HashMap<String, Instant> = HashMap::new();
@@ -1209,6 +1286,7 @@ fn run(once: bool, print_only: bool) -> Result<()> {
let mut pending_samples = Vec::new();
let mut report_failures = 0_u32;
let mut next_update_check = Instant::now();
let mut config_hash = String::new();
loop {
let current = Instant::now();
let due = config
@@ -1252,27 +1330,28 @@ fn run(once: bool, print_only: bool) -> Result<()> {
}
if Instant::now() >= next_report && !pending_samples.is_empty() {
match submit(&agent, &config, &pending_samples) {
Ok(remote) => {
match submit(&agent, &config, &pending_samples, &config_hash) {
Ok(result) => {
pending_samples.clear();
report_failures = 0;
if let Some(remote) = remote {
if !once && remote.auto_update == 1 && Instant::now() >= next_update_check {
match update(&agent) {
Ok(true) => return Ok(()),
Ok(false) => {
next_update_check = Instant::now() + UPDATE_CHECK_INTERVAL;
}
Err(error) => {
eprintln!("agent update failed: {error}");
next_update_check = Instant::now() + UPDATE_RETRY_INTERVAL;
}
}
}
config_hash = result.config_hash;
if let Some(remote) = result.config {
if apply_remote(&mut config, &remote) {
next_latency.clear();
}
}
if !once && config.auto_update && Instant::now() >= next_update_check {
match update(&agent) {
Ok(true) => return Ok(()),
Ok(false) => {
next_update_check = Instant::now() + UPDATE_CHECK_INTERVAL;
}
Err(error) => {
eprintln!("agent update failed: {error}");
next_update_check = Instant::now() + UPDATE_RETRY_INTERVAL;
}
}
}
}
Err(error) => {
eprintln!("report failed: {error}");
@@ -1329,10 +1408,10 @@ mod tests {
use std::time::Duration;
use super::{
agent_artifact_name, executable_format_valid, median, normalized_version, parse_checksum,
parse_probe_target, ping_latency, prune_report_samples, report_retry_delay,
sanitize_latency_tasks, selected_interface, tcp_latency_probe, valid_worker_url,
version_triplet, LatencyTask, Report, MAX_LATENCY_TASKS, PROBE_ATTEMPTS,
agent_artifact_name, executable_format_valid, is_public_probe_ip, median,
normalized_version, parse_checksum, parse_probe_target, ping_latency, prune_report_samples,
report_retry_delay, sanitize_latency_tasks, selected_interface, tcp_latency_probe_address,
valid_worker_url, version_triplet, LatencyTask, Report, MAX_LATENCY_TASKS, PROBE_ATTEMPTS,
};
#[cfg(any(target_os = "windows", target_os = "macos"))]
@@ -1380,6 +1459,10 @@ mod tests {
fn validates_worker_urls() {
assert!(valid_worker_url("https://monitor.example.com/"));
assert!(valid_worker_url("http://127.0.0.1:8787"));
assert!(valid_worker_url("http://localhost:8787"));
assert!(valid_worker_url("http://[::1]:8787"));
assert!(!valid_worker_url("http://monitor.example.com"));
assert!(!valid_worker_url("http://127.0.0.2:8787"));
assert!(!valid_worker_url("monitor.example.com"));
assert!(!valid_worker_url("https://"));
assert!(!valid_worker_url("https://user@example.com"));
@@ -1475,8 +1558,8 @@ mod tests {
Some(("example.com".to_string(), 443))
);
assert_eq!(
parse_probe_target("127.0.0.1:8080"),
Some(("127.0.0.1".to_string(), 8080))
parse_probe_target("1.1.1.1:8080"),
Some(("1.1.1.1".to_string(), 8080))
);
for target in [
"",
@@ -1484,6 +1567,11 @@ mod tests {
"example.com:0",
"example.com:65536",
"999.1.1.1",
"127.0.0.1:8080",
"169.254.169.254",
"192.168.1.1",
"router.local",
"localhost",
"[::1]:443",
"bad host",
] {
@@ -1491,6 +1579,29 @@ mod tests {
}
}
#[test]
fn accepts_only_public_probe_addresses() {
for address in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"] {
assert!(is_public_probe_ip(address.parse().expect("IP address")));
}
for address in [
"0.0.0.0",
"10.0.0.1",
"100.64.0.1",
"127.0.0.1",
"169.254.169.254",
"172.16.0.1",
"192.168.1.1",
"198.18.0.1",
"224.0.0.1",
"::1",
"fe80::1",
"fd00:ec2::254",
] {
assert!(!is_public_probe_ip(address.parse().expect("IP address")));
}
}
#[test]
fn calculates_median() {
assert_eq!(median(&mut [4.0, 1.0, 3.0]), 3.0);
@@ -1506,11 +1617,10 @@ mod tests {
listener.accept().expect("accept probe connection");
}
});
let (latency, loss) = tcp_latency_probe(&address.to_string());
let (latency, loss) = tcp_latency_probe_address(&address);
accepter.join().expect("join listener");
assert!(latency >= 0.0);
assert_eq!(loss, 0.0);
assert_eq!(tcp_latency_probe(""), (-1.0, -1.0));
}
#[test]