Compare commits
22 Commits
v0.3.0
...
fix/pusher
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fe5fd3139 | |||
| b46f85f436 | |||
|
190cac929e
|
|||
|
5e1d0d7c54
|
|||
|
9081e08785
|
|||
|
06db6f1d48
|
|||
|
ba8f828e89
|
|||
|
9abfad29b9
|
|||
|
bc9ec1a48c
|
|||
|
c461232095
|
|||
|
fbe61a5862
|
|||
|
56696050ec
|
|||
| 638f00bf0c | |||
| da2124031e | |||
| 538e72f806 | |||
| 4fc0790fe7 | |||
| 237e62d4be | |||
| fa2f458468 | |||
| 167869b881 | |||
| 0fdefcfd0f | |||
| df8effcc60 | |||
| 69d877a360 |
@@ -3,7 +3,7 @@
|
|||||||
# DO NOT commit the real token to git!
|
# DO NOT commit the real token to git!
|
||||||
# This file is in .gitignore and should not be committed to git
|
# This file is in .gitignore and should not be committed to git
|
||||||
GITEA_API_TOKEN=your_gitea_api_token_here
|
GITEA_API_TOKEN=your_gitea_api_token_here
|
||||||
# Example: GITEA_API_TOKEN=***REMOVED***
|
# Example: GITEA_API_TOKEN=5cfa8c33e337ebaadb355c0ffa2d053d521ee43b
|
||||||
# (replace with your actual token after revoking the old one)
|
# (replace with your actual token after revoking the old one)
|
||||||
|
|
||||||
# === bal-server ===
|
# === bal-server ===
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,6 +8,9 @@
|
|||||||
.env.production
|
.env.production
|
||||||
.env.secret
|
.env.secret
|
||||||
|
|
||||||
|
# Shell scripts that load env vars (contain secrets, local only)
|
||||||
|
bal-pusher.sh
|
||||||
|
bal-server.sh
|
||||||
|
|
||||||
# Private keys - NEVER commit to git
|
# Private keys - NEVER commit to git
|
||||||
# Only public_key.pem should be tracked (if needed)
|
# Only public_key.pem should be tracked (if needed)
|
||||||
@@ -36,4 +39,3 @@ Cargo.lock
|
|||||||
!lib/
|
!lib/
|
||||||
!contrib/
|
!contrib/
|
||||||
!src/
|
!src/
|
||||||
make_release.sh
|
|
||||||
|
|||||||
273
Cargo.lock
generated
273
Cargo.lock
generated
@@ -325,11 +325,12 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ed25519-dalek",
|
"confy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hex",
|
"hex",
|
||||||
"hex-conservative 0.1.1",
|
"hex-conservative 0.1.1",
|
||||||
"log",
|
"log",
|
||||||
|
"openssl",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -363,12 +364,6 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64ct"
|
|
||||||
version = "1.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bech32"
|
name = "bech32"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -597,10 +592,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "confy"
|
||||||
version = "0.9.6"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0"
|
||||||
|
dependencies = [
|
||||||
|
"directories",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
@@ -746,33 +747,6 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "curve25519-dalek"
|
|
||||||
version = "4.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures 0.2.17",
|
|
||||||
"curve25519-dalek-derive",
|
|
||||||
"digest 0.10.7",
|
|
||||||
"fiat-crypto",
|
|
||||||
"rustc_version",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "curve25519-dalek-derive"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@@ -786,17 +760,6 @@ dependencies = [
|
|||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "der"
|
|
||||||
version = "0.7.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
|
||||||
dependencies = [
|
|
||||||
"const-oid 0.9.6",
|
|
||||||
"pem-rfc7468",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -846,7 +809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.1",
|
"block-buffer 0.12.1",
|
||||||
"const-oid 0.10.2",
|
"const-oid",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -861,6 +824,27 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "directories"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -872,30 +856,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519"
|
|
||||||
version = "2.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
|
||||||
dependencies = [
|
|
||||||
"pkcs8",
|
|
||||||
"signature",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519-dalek"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek",
|
|
||||||
"ed25519",
|
|
||||||
"serde",
|
|
||||||
"sha2",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -956,12 +916,6 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fiat-crypto"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -1624,6 +1578,16 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1795,6 +1759,15 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.5.4+3.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.110"
|
version = "0.9.110"
|
||||||
@@ -1803,10 +1776,17 @@ checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1830,15 +1810,6 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pem-rfc7468"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -1857,16 +1828,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkcs8"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
|
||||||
dependencies = [
|
|
||||||
"der",
|
|
||||||
"spki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@@ -2033,6 +1994,17 @@ dependencies = [
|
|||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.14",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.4"
|
version = "1.10.4"
|
||||||
@@ -2354,15 +2326,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signature"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
@@ -2410,16 +2373,6 @@ dependencies = [
|
|||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spki"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"der",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlite"
|
name = "sqlite"
|
||||||
version = "0.34.0"
|
version = "0.34.0"
|
||||||
@@ -2546,6 +2499,26 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.59"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.59"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.44"
|
version = "0.3.44"
|
||||||
@@ -3059,6 +3032,15 @@ dependencies = [
|
|||||||
"windows-link 0.1.1",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -3086,6 +3068,21 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.48.5",
|
||||||
|
"windows_aarch64_msvc 0.48.5",
|
||||||
|
"windows_i686_gnu 0.48.5",
|
||||||
|
"windows_i686_msvc 0.48.5",
|
||||||
|
"windows_x86_64_gnu 0.48.5",
|
||||||
|
"windows_x86_64_gnullvm 0.48.5",
|
||||||
|
"windows_x86_64_msvc 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3119,6 +3116,12 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.53.1",
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3131,6 +3134,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3143,6 +3152,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3167,6 +3182,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3179,6 +3200,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3191,6 +3218,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3203,6 +3236,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
|
|||||||
42
Cargo.toml
42
Cargo.toml
@@ -4,55 +4,39 @@ version = "0.3.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
|
||||||
default = ["server", "pusher"]
|
|
||||||
server = ["dep:actix-web", "dep:actix-governor", "dep:actix-rt", "dep:chrono", "dep:hex-conservative"]
|
|
||||||
pusher = ["dep:zmq", "dep:reqwest", "dep:byteorder", "dep:base64", "dep:ed25519-dalek"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = { version = "0.22.1" }
|
||||||
bs58 = { version = "0.4.0" }
|
bs58 = { version = "0.4.0" }
|
||||||
bytes = { version = "1.2" }
|
bytes = { version = "1.2" }
|
||||||
bitcoin = { version = "0.32.5" }
|
bitcoin = { version = "0.32.5" }
|
||||||
bitcoincore-rpc = { version = "0.19.0" }
|
bitcoincore-rpc = { version = "0.19.0" }
|
||||||
bitcoincore-rpc-json = { version = "0.19.0" }
|
bitcoincore-rpc-json = { version = "0.19.0" }
|
||||||
|
byteorder = { version = "1.5.0" }
|
||||||
|
confy = { version = "0.6.1" }
|
||||||
|
chrono = { version = "0.4.40" }
|
||||||
env_logger = { version = "0.11.5" }
|
env_logger = { version = "0.11.5" }
|
||||||
hex = { version = "0.4.3" }
|
hex = { version = "0.4.3" }
|
||||||
|
hex-conservative = { version = "0.1.1" }
|
||||||
|
actix-web = { version = "4.9.0" }
|
||||||
|
actix-governor = { version = "0.6.0" }
|
||||||
log = { version = "0.4.21" }
|
log = { version = "0.4.21" }
|
||||||
|
openssl = { version = "0.10.74", features = ["vendored"] }
|
||||||
|
sha2 = { version = "0.10.8" }
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.116" }
|
serde_json = { version = "1.0.116" }
|
||||||
sqlite = { version = "0.34.0" }
|
sqlite = { version = "0.34.0" }
|
||||||
regex = { version = "1.10.4" }
|
regex = { version = "1.10.4" }
|
||||||
|
reqwest = { version = "0.12.24", features = ["json","socks"] }
|
||||||
|
actix-rt = { version = "2.10.0" }
|
||||||
tokio = { version = "1", features = ["rt", "net","macros","rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt", "net","macros","rt-multi-thread"] }
|
||||||
url = { version = "2" }
|
url = { version = "2" }
|
||||||
|
zmq = { version = "0.10.0" }
|
||||||
# server-only
|
|
||||||
actix-web = { version = "4.9.0", optional = true }
|
|
||||||
actix-governor = { version = "0.6.0", optional = true }
|
|
||||||
actix-rt = { version = "2.10.0", optional = true }
|
|
||||||
chrono = { version = "0.4.40", optional = true }
|
|
||||||
hex-conservative = { version = "0.1.1", optional = true }
|
|
||||||
|
|
||||||
# pusher-only
|
|
||||||
zmq = { version = "0.10.0", optional = true }
|
|
||||||
reqwest = { version = "0.12.24", features = ["json","socks"], optional = true }
|
|
||||||
byteorder = { version = "1.5.0", optional = true }
|
|
||||||
base64 = { version = "0.22.1", optional = true }
|
|
||||||
ed25519-dalek = { version = "2", features = ["pem", "pkcs8"], optional = true }
|
|
||||||
sha2 = { version = "0.10.8" }
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
strip = true
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bal-server"
|
name = "bal-server"
|
||||||
path = "src/bin/bal-server.rs"
|
path = "src/bin/bal-server.rs"
|
||||||
required-features = ["server"]
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bal-pusher"
|
name = "bal-pusher"
|
||||||
path = "src/bin/bal-pusher.rs"
|
path = "src/bin/bal-pusher.rs"
|
||||||
required-features = ["pusher"]
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export RUST_LOG=trace
|
|
||||||
|
|
||||||
export BAL_PUSHER_DB_FILE="$(pwd)/bal.db"
|
|
||||||
#export BAL_PUSHER_BITCOIN_COOKIE_FILE=/~/.bitcoin/.cookie
|
|
||||||
#export BAL_PUSHER_REGTEST_COOKIE_FILE=/~/.bitcoin/regtest/.cookie
|
|
||||||
#export BAL_PUSHER_TESTNET_COOKIE_FILE=/~/.bitcoin/testnet3/.cookie
|
|
||||||
#export BAL_PUSHER_SIGNET_COOKIE_FILE=/~/.bitcoin/signet/.cookie
|
|
||||||
|
|
||||||
export BAL_PUSHER_REGTEST_ZMQ_HASHBLOCK=tcp://127.0.0.1:21332
|
|
||||||
export BAL_PUSHER_SEND_STATS=true
|
|
||||||
export WELIST_SERVER_URL=http://localhost:8086
|
|
||||||
export WELIST_SKIP_URL_VALIDATION=true
|
|
||||||
export BAL_SERVER_URL="http://127.0.0.1:9133"
|
|
||||||
export SSL_KEY_PATH="$(pwd)/private_key.pem"
|
|
||||||
cargo run --bin=bal-pusher regtest --features=pusher
|
|
||||||
@@ -129,7 +129,7 @@ bitcoin-cli -regtest gettransaction ... | bitcoin-cli -regtest sendrawtransactio
|
|||||||
- It is a helper script that wraps `bitcoin-cli` to send a pre-created transaction, get the raw bytes, and send them to a new address. It is only useful for manual testing and integration checks.
|
- It is a helper script that wraps `bitcoin-cli` to send a pre-created transaction, get the raw bytes, and send them to a new address. It is only useful for manual testing and integration checks.
|
||||||
|
|
||||||
### `make_release.sh` (Release Builder)
|
### `make_release.sh` (Release Builder)
|
||||||
This script builds a release binary, creates a Git tag, and uploads the release to a Git server (Gitea). It also hardcodes a Gitea API token (`TOKEN="***REMOVED***"`), which is a major security risk.
|
This script builds a release binary, creates a Git tag, and uploads the release to a Git server (Gitea). It also hardcodes a Gitea API token (`TOKEN="5cfa8c33e337ebaadb355c0ffa2d053d521ee43b"`), which is a major security risk.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# WARNING: This script contains a hardcoded secret token. Do not use it as-is for production.
|
# WARNING: This script contains a hardcoded secret token. Do not use it as-is for production.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Regression tests: `tests/panic_regression_tests.rs` (2 tests).
|
|||||||
### 3. Secret Leakage (HIGH)
|
### 3. Secret Leakage (HIGH)
|
||||||
**Location:** `make_release.sh`, `contrib/download_and_install_bal.sh`, `private_key.pem`, `privkey.pem`, `ec.key`, `chiave_privata.key`.
|
**Location:** `make_release.sh`, `contrib/download_and_install_bal.sh`, `private_key.pem`, `privkey.pem`, `ec.key`, `chiave_privata.key`.
|
||||||
**Description:**
|
**Description:**
|
||||||
- The `make_release.sh` script contains a hardcoded Gitea API token: `TOKEN="***REMOVED***"`. If the script is accidentally pushed to a public repository, it will be visible to everyone.
|
- The `make_release.sh` script contains a hardcoded Gitea API token: `TOKEN="5cfa8c33e337ebaadb355c0ffa2d053d521ee43b"`. If the script is accidentally pushed to a public repository, it will be visible to everyone.
|
||||||
- The `contrib/download_and_install_bal.sh` script contains a hardcoded `xpub` address and a fixed fee. This is less critical but could be used for fingerprinting.
|
- The `contrib/download_and_install_bal.sh` script contains a hardcoded `xpub` address and a fixed fee. This is less critical but could be used for fingerprinting.
|
||||||
- The `private_key.pem` and `chiave_privata.key` files are stored in the project root (and in the repository). If the repository is public, the private key is compromised. An attacker could use this to sign fake statistics or forge authentication credentials.
|
- The `private_key.pem` and `chiave_privata.key` files are stored in the project root (and in the repository). If the repository is public, the private key is compromised. An attacker could use this to sign fake statistics or forge authentication credentials.
|
||||||
**Impact:** An attacker could gain unauthorized access to the CI/CD pipeline, the release server, or the `welist` statistics service.
|
**Impact:** An attacker could gain unauthorized access to the CI/CD pipeline, the release server, or the `welist` statistics service.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use bitcoincore_rpc::{Auth, Client, Error, RpcApi, bitcoin};
|
|||||||
use bitcoincore_rpc_json::GetBlockchainInfoResult;
|
use bitcoincore_rpc_json::GetBlockchainInfoResult;
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use ed25519_dalek::{Signer as _, SigningKey, pkcs8::DecodePrivateKey};
|
use hex;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -23,7 +23,12 @@ use zmq::{Context, DEALER, DONTWAIT, Socket};
|
|||||||
use bal_server::db::open_db;
|
use bal_server::db::open_db;
|
||||||
use bal_server::validation::is_valid_welist_url;
|
use bal_server::validation::is_valid_welist_url;
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
|
use openssl::hash::MessageDigest;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use openssl::sign::Signer;
|
||||||
|
use openssl::sign::Verifier;
|
||||||
use reqwest::Client as rClient;
|
use reqwest::Client as rClient;
|
||||||
|
use std::fs;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -140,7 +145,7 @@ fn get_network_params_default(network: Network) -> NetworkParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdError>> {
|
fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdError>> {
|
||||||
if !network.cookie_file.is_empty() {
|
if network.cookie_file != "" {
|
||||||
Ok(network.cookie_file.clone())
|
Ok(network.cookie_file.clone())
|
||||||
} else {
|
} else {
|
||||||
match env::var_os("HOME") {
|
match env::var_os("HOME") {
|
||||||
@@ -158,12 +163,12 @@ fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client_from_username(
|
fn get_client_from_username(
|
||||||
url: &str,
|
url: &String,
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
if !network.rpc_user.is_empty() {
|
if network.rpc_user != "" {
|
||||||
match Client::new(
|
match Client::new(
|
||||||
url,
|
&url[..],
|
||||||
Auth::UserPass(network.rpc_user.to_string(), network.rpc_pass.to_string()),
|
Auth::UserPass(network.rpc_user.to_string(), network.rpc_pass.to_string()),
|
||||||
) {
|
) {
|
||||||
Ok(client) => match client.get_blockchain_info() {
|
Ok(client) => match client.get_blockchain_info() {
|
||||||
@@ -177,30 +182,30 @@ fn get_client_from_username(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client_from_cookie(
|
fn get_client_from_cookie(
|
||||||
url: &str,
|
url: &String,
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
match get_cookie_filename(network) {
|
match get_cookie_filename(network) {
|
||||||
Ok(cookie) => match Client::new(url, Auth::CookieFile(cookie.into())) {
|
Ok(cookie) => match Client::new(&url[..], Auth::CookieFile(cookie.into())) {
|
||||||
Ok(client) => match client.get_blockchain_info() {
|
Ok(client) => match client.get_blockchain_info() {
|
||||||
Ok(bcinfo) => Ok((client, bcinfo)),
|
Ok(bcinfo) => Ok((client, bcinfo)),
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client(
|
fn get_client(
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
let url = format!("{}:{}/", network.host, network.port);
|
let url = format!("{}:{}/", network.host, &network.port);
|
||||||
debug!("trying to connect to bitcoin daemon:{url}");
|
debug!("trying to connect to bitcoin daemon:{url}");
|
||||||
match get_client_from_username(&url, network) {
|
match get_client_from_username(&url, network) {
|
||||||
Ok(client) => Ok(client),
|
Ok(client) => Ok(client),
|
||||||
Err(_) => match get_client_from_cookie(&url, network) {
|
Err(_) => match get_client_from_cookie(&url, &network) {
|
||||||
Ok(client) => Ok(client),
|
Ok(client) => Ok(client),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err.into()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +267,7 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|||||||
let mut invalid_txs: std::collections::HashMap<String, String> = HashMap::new();
|
let mut invalid_txs: std::collections::HashMap<String, String> = HashMap::new();
|
||||||
for row_result in match query_tx.bind::<&[(_, Value)]>(
|
for row_result in match query_tx.bind::<&[(_, Value)]>(
|
||||||
&[
|
&[
|
||||||
(":locktime_threshold", LOCKTIME_THRESHOLD.into()),
|
(":locktime_threshold", (LOCKTIME_THRESHOLD as i64).into()),
|
||||||
(":bestblock_time", (average_time as i64).into()),
|
(":bestblock_time", (average_time as i64).into()),
|
||||||
(":bestblock_height", (bcinfo.blocks as i64).into()),
|
(":bestblock_height", (bcinfo.blocks as i64).into()),
|
||||||
(":network", network_params.db_field.clone().into()),
|
(":network", network_params.db_field.clone().into()),
|
||||||
@@ -329,7 +334,9 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|||||||
let _ = stmt.next();
|
let _ = stmt.next();
|
||||||
}
|
}
|
||||||
if let Err(e) = send_stats_report(cfg, bcinfo).await {
|
if let Err(e) = send_stats_report(cfg, bcinfo).await {
|
||||||
error!("send_stats_report failed: {}", e);
|
// Never discard silently: a failing report is otherwise
|
||||||
|
// invisible in the logs and can go unnoticed for a long time.
|
||||||
|
warn!("send_stats_report failed: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) = calculate_stats(&db, network_params.db_field.clone()).await {
|
if let Err(e) = calculate_stats(&db, network_params.db_field.clone()).await {
|
||||||
warn!("calculate_stats failed: {e}");
|
warn!("calculate_stats failed: {e}");
|
||||||
@@ -469,38 +476,30 @@ async fn welist_http_client(welist_url: &str) -> rClient {
|
|||||||
.parse::<bool>()
|
.parse::<bool>()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !prefer_ipv6 {
|
if !prefer_ipv6 {
|
||||||
return new_welist_client();
|
return rClient::new();
|
||||||
}
|
}
|
||||||
let (host, port) = match parse_host_port(welist_url) {
|
let (host, port) = match parse_host_port(welist_url) {
|
||||||
Some(hp) => hp,
|
Some(hp) => hp,
|
||||||
None => {
|
None => {
|
||||||
warn!("BAL_PUSHER_PREFER_IPV6: cannot parse '{welist_url}', using default resolver");
|
warn!("BAL_PUSHER_PREFER_IPV6: cannot parse '{welist_url}', using default resolver");
|
||||||
return new_welist_client();
|
return rClient::new();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match resolve_first_ipv6(&host, port).await {
|
match resolve_first_ipv6(&host, port).await {
|
||||||
Some(addr) => {
|
Some(addr) => {
|
||||||
debug!("BAL_PUSHER_PREFER_IPV6: pinning {host} to {addr}");
|
debug!("BAL_PUSHER_PREFER_IPV6: pinning {host} to {addr}");
|
||||||
rClient::builder()
|
rClient::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
|
||||||
.resolve(&host, addr)
|
.resolve(&host, addr)
|
||||||
.build()
|
.build()
|
||||||
.unwrap_or_else(|_| new_welist_client())
|
.unwrap_or_else(|_| rClient::new())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
debug!("BAL_PUSHER_PREFER_IPV6: no IPv6 address for {host}, using default resolver");
|
debug!("BAL_PUSHER_PREFER_IPV6: no IPv6 address for {host}, using default resolver");
|
||||||
new_welist_client()
|
rClient::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_welist_client() -> rClient {
|
|
||||||
rClient::builder()
|
|
||||||
.timeout(Duration::from_secs(10))
|
|
||||||
.build()
|
|
||||||
.unwrap_or_else(|_| rClient::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_stats_report(
|
async fn send_stats_report(
|
||||||
cfg: &MyConfig,
|
cfg: &MyConfig,
|
||||||
bcinfo: GetBlockchainInfoResult,
|
bcinfo: GetBlockchainInfoResult,
|
||||||
@@ -509,11 +508,7 @@ async fn send_stats_report(
|
|||||||
debug!("sending report to welist");
|
debug!("sending report to welist");
|
||||||
let welist_url = env::var("WELIST_SERVER_URL")
|
let welist_url = env::var("WELIST_SERVER_URL")
|
||||||
.unwrap_or("https://welist.bitcoin-after.life".to_string());
|
.unwrap_or("https://welist.bitcoin-after.life".to_string());
|
||||||
let skip_validation = env::var("WELIST_SKIP_URL_VALIDATION")
|
if !is_valid_welist_url(&welist_url) {
|
||||||
.unwrap_or("false".to_string())
|
|
||||||
.parse::<bool>()
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !skip_validation && !is_valid_welist_url(&welist_url) {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Invalid or unsafe WELIST_SERVER_URL: {}. Skipping stats report.",
|
"Invalid or unsafe WELIST_SERVER_URL: {}. Skipping stats report.",
|
||||||
welist_url
|
welist_url
|
||||||
@@ -529,7 +524,7 @@ async fn send_stats_report(
|
|||||||
cfg.url, chain, bcinfo.blocks, bcinfo.median_time, bcinfo.best_block_hash
|
cfg.url, chain, bcinfo.blocks, bcinfo.median_time, bcinfo.best_block_hash
|
||||||
);
|
);
|
||||||
trace!("message to be sent: {}", message);
|
trace!("message to be sent: {}", message);
|
||||||
let sign = sign_message(cfg.ssl_key_path.as_str(), message.as_str());
|
let sign = sign_message(cfg.ssl_key_path.as_str(), &message.as_str());
|
||||||
let response = client
|
let response = client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("User-Agent", format!("bal-pusher/{}", VERSION))
|
.header("User-Agent", format!("bal-pusher/{}", VERSION))
|
||||||
@@ -544,23 +539,32 @@ async fn send_stats_report(
|
|||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
let status = response.status();
|
if !response.status().is_success() {
|
||||||
let body = response.text().await?;
|
warn!(
|
||||||
info!(
|
"Non-success response: {} {}",
|
||||||
"Report to welist({}) status={} body={}",
|
response.status(),
|
||||||
welist_url, status, body
|
response.status().canonical_reason().unwrap_or("")
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = &(response.text().await?);
|
||||||
|
info!("Report to welist({})\tSent: {}", welist_url, body);
|
||||||
} else {
|
} else {
|
||||||
debug!("Not sending stats");
|
debug!("Not sending stats");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn sign_message(private_key_path: &str, message: &str) -> String {
|
fn sign_message(private_key_path: &str, message: &str) -> String {
|
||||||
let signing_key =
|
let key_data = fs::read(private_key_path).unwrap();
|
||||||
SigningKey::read_pkcs8_pem_file(private_key_path).expect("failed to parse private key PEM");
|
|
||||||
let signature = signing_key.sign(message.as_bytes());
|
|
||||||
|
|
||||||
general_purpose::STANDARD.encode(signature.to_bytes())
|
let private_key = PKey::private_key_from_pem(&key_data).unwrap();
|
||||||
|
let mut signer = Signer::new_without_digest(&private_key).unwrap();
|
||||||
|
|
||||||
|
let signature = signer.sign_oneshot_to_vec(message.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let signature_b64 = general_purpose::STANDARD.encode(&signature);
|
||||||
|
|
||||||
|
signature_b64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_env(cfg: &mut MyConfig) {
|
fn parse_env(cfg: &mut MyConfig) {
|
||||||
@@ -579,45 +583,71 @@ fn parse_env_netconfig(cfg_lock: &mut MyConfig, chain: &str) -> NetworkParams {
|
|||||||
"testnet4" => &mut cfg_lock.testnet4,
|
"testnet4" => &mut cfg_lock.testnet4,
|
||||||
&_ => &mut cfg_lock.mainnet,
|
&_ => &mut cfg_lock.mainnet,
|
||||||
};
|
};
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_HOST", chain.to_uppercase())) {
|
match env::var(format!("BAL_PUSHER_{}_HOST", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.host = value;
|
cfg.host = value;
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_PORT", chain.to_uppercase()))
|
Err(_) => {}
|
||||||
&& let Ok(port_num) = value.parse::<u64>()
|
}
|
||||||
{
|
match env::var(format!("BAL_PUSHER_{}_PORT", chain.to_uppercase())) {
|
||||||
if let Ok(port) = u16::try_from(port_num) {
|
Ok(value) => match value.parse::<u64>() {
|
||||||
cfg.port = port;
|
Ok(value) => match u16::try_from(value) {
|
||||||
} else {
|
Ok(port) => cfg.port = port,
|
||||||
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"Port value {} exceeds u16 range for chain {}",
|
"Port value {} exceeds u16 range for chain {}: {}",
|
||||||
port_num, chain
|
value, chain, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {}
|
||||||
|
},
|
||||||
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_DIR_PATH", chain.to_uppercase())) {
|
match env::var(format!("BAL_PUSHER_{}_DIR_PATH", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.dir_path = value;
|
cfg.dir_path = value;
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_DB_FIELD", chain.to_uppercase())) {
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
match env::var(format!("BAL_PUSHER_{}_DB_FIELD", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.db_field = value;
|
cfg.db_field = value;
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_COOKIE_FILE", chain.to_uppercase())) {
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
match env::var(format!("BAL_PUSHER_{}_COOKIE_FILE", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.cookie_file = value;
|
cfg.cookie_file = value;
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_RPC_USER", chain.to_uppercase())) {
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
match env::var(format!("BAL_PUSHER_{}_RPC_USER", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.rpc_user = value;
|
cfg.rpc_user = value;
|
||||||
}
|
}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_RPC_PASSWORD", chain.to_uppercase())) {
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
match env::var(format!("BAL_PUSHER_{}_RPC_PASSWORD", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
cfg.rpc_pass = value;
|
cfg.rpc_pass = value;
|
||||||
}
|
}
|
||||||
println!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase());
|
Err(_) => {}
|
||||||
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())) {
|
}
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())
|
||||||
|
);
|
||||||
|
match env::var(format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())) {
|
||||||
|
Ok(value) => {
|
||||||
println!("value:{}", value);
|
println!("value:{}", value);
|
||||||
cfg.zmq_listener = value;
|
cfg.zmq_listener = value;
|
||||||
}
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
cfg.clone()
|
cfg.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn check_zmq_connection(endpoint: &str) -> bool {
|
fn check_zmq_connection(endpoint: &str) -> bool {
|
||||||
trace!("check zmq connection");
|
trace!("check zmq connection");
|
||||||
let context = Context::new();
|
let context = Context::new();
|
||||||
@@ -635,7 +665,6 @@ fn check_zmq_connection(endpoint: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add this struct to monitor connection health
|
// Add this struct to monitor connection health
|
||||||
#[allow(dead_code)]
|
|
||||||
struct ConnectionMonitor {
|
struct ConnectionMonitor {
|
||||||
last_message_time: Instant,
|
last_message_time: Instant,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
@@ -643,7 +672,6 @@ struct ConnectionMonitor {
|
|||||||
max_consecutive_timeouts: u32,
|
max_consecutive_timeouts: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl ConnectionMonitor {
|
impl ConnectionMonitor {
|
||||||
fn new(timeout_secs: u64, max_timeouts: u32) -> Self {
|
fn new(timeout_secs: u64, max_timeouts: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -681,7 +709,6 @@ impl ConnectionMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
enum ConnectionStatus {
|
enum ConnectionStatus {
|
||||||
Healthy,
|
Healthy,
|
||||||
Warning(Duration),
|
Warning(Duration),
|
||||||
@@ -693,6 +720,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut cfg = MyConfig::default();
|
let mut cfg = MyConfig::default();
|
||||||
|
|
||||||
|
let dbfile = env::var("BAL_PUSHER_DB_FILE").unwrap();
|
||||||
parse_env(&mut cfg);
|
parse_env(&mut cfg);
|
||||||
let mut args = std::env::args();
|
let mut args = std::env::args();
|
||||||
let _exe_name = args.next().unwrap();
|
let _exe_name = args.next().unwrap();
|
||||||
@@ -727,48 +755,31 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match socket.set_subscribe(b"") {
|
match socket.set_subscribe(b"") {
|
||||||
Ok(_) => {
|
Ok(_) => {}
|
||||||
info!("ZMQ subscribed to all topics on {}", zmq_address);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("ZMQ subscribe failed: {}, exiting", e);
|
error!("ZMQ subscribe failed: {}, exiting", e);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = main_result(&cfg, network_params).await {
|
let _ = main_result(&cfg, network_params).await;
|
||||||
error!("main_result failed on startup: {}", e);
|
|
||||||
}
|
|
||||||
info!("waiting new blocks..");
|
info!("waiting new blocks..");
|
||||||
|
let mut last_seq: Vec<u8> = [0; 4].to_vec();
|
||||||
|
let mut counter = 0;
|
||||||
|
let max = 100;
|
||||||
socket.set_rcvtimeo(5000).unwrap(); // 5 seconds timeout
|
socket.set_rcvtimeo(5000).unwrap(); // 5 seconds timeout
|
||||||
let mut consecutive_timeouts: u32 = 0;
|
|
||||||
loop {
|
loop {
|
||||||
let message = match socket.recv_multipart(0) {
|
let message = match socket.recv_multipart(0) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
consecutive_timeouts += 1;
|
|
||||||
if consecutive_timeouts == 1 {
|
|
||||||
warn!("ZMQ recv timeout or error: {}, retrying...", e);
|
warn!("ZMQ recv timeout or error: {}, retrying...", e);
|
||||||
} else if consecutive_timeouts.is_multiple_of(12) {
|
|
||||||
warn!(
|
|
||||||
"No ZMQ messages for {}s ({} consecutive timeouts), is bitcoind ZMQ active on {}?",
|
|
||||||
consecutive_timeouts * 5,
|
|
||||||
consecutive_timeouts,
|
|
||||||
zmq_address
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if consecutive_timeouts > 0 {
|
|
||||||
info!(
|
|
||||||
"ZMQ connection restored after {} consecutive timeouts",
|
|
||||||
consecutive_timeouts
|
|
||||||
);
|
|
||||||
}
|
|
||||||
consecutive_timeouts = 0;
|
|
||||||
let topic = message[0].clone();
|
let topic = message[0].clone();
|
||||||
let body = message[1].clone();
|
let body = message[1].clone();
|
||||||
|
let seq = message[2].clone();
|
||||||
|
last_seq = seq;
|
||||||
debug!(
|
debug!(
|
||||||
"ZMQ:GET TOPIC: {}",
|
"ZMQ:GET TOPIC: {}",
|
||||||
String::from_utf8(topic.clone()).expect("invalid topic")
|
String::from_utf8(topic.clone()).expect("invalid topic")
|
||||||
@@ -776,15 +787,12 @@ async fn main() -> std::io::Result<()> {
|
|||||||
trace!("ZMQ:GET BODY: {}", hex::encode(&body));
|
trace!("ZMQ:GET BODY: {}", hex::encode(&body));
|
||||||
if topic == b"hashblock" {
|
if topic == b"hashblock" {
|
||||||
info!("NEW BLOCK: {}", hex::encode(&body));
|
info!("NEW BLOCK: {}", hex::encode(&body));
|
||||||
if let Err(e) = main_result(&cfg, network_params).await {
|
let _ = main_result(&cfg, network_params).await;
|
||||||
error!("main_result failed on new block: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(100)); // Sleep for 100ms
|
thread::sleep(Duration::from_millis(100)); // Sleep for 100ms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
fn seq_to_str(seq: &Vec<u8>) -> String {
|
||||||
fn seq_to_str(seq: &[u8]) -> String {
|
|
||||||
if seq.len() == 4 {
|
if seq.len() == 4 {
|
||||||
let mut rdr = Cursor::new(seq);
|
let mut rdr = Cursor::new(seq);
|
||||||
let sequence = rdr
|
let sequence = rdr
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use chrono::Utc;
|
|||||||
use hex_conservative::FromHex;
|
use hex_conservative::FromHex;
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
use sqlite::State;
|
use sqlite::State;
|
||||||
use sqlite::{Connection, Value};
|
use sqlite::{Connection, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -118,7 +119,6 @@ pub struct StatsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
struct ActixConfig {
|
struct ActixConfig {
|
||||||
max_body_size: usize,
|
max_body_size: usize,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
@@ -208,7 +208,7 @@ async fn echo_pub_key(data: web::Data<AppState>) -> impl Responder {
|
|||||||
"Failed to read public key file {}: {}",
|
"Failed to read public key file {}: {}",
|
||||||
data.cfg.pub_key_path, e
|
data.cfg.pub_key_path, e
|
||||||
);
|
);
|
||||||
HttpResponse::InternalServerError().body("error")
|
HttpResponse::InternalServerError().body("Failed to read public key file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,13 +224,13 @@ async fn echo_info(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("error");
|
return HttpResponse::NotFound().body("Unknown network");
|
||||||
}
|
}
|
||||||
info!("echo info!!!{}", param);
|
info!("echo info!!!{}", param);
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
debug!("network disabled {}", param);
|
debug!("network disabled {}", param);
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("network disabled");
|
||||||
}
|
}
|
||||||
let remote_addr = req
|
let remote_addr = req
|
||||||
.headers()
|
.headers()
|
||||||
@@ -257,7 +257,7 @@ async fn echo_info(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_info (lookup phase)");
|
error!("DB mutex poisoned in echo_info (lookup phase)");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match get_last_used_address_by_ip(
|
match get_last_used_address_by_ip(
|
||||||
@@ -275,7 +275,10 @@ async fn echo_info(
|
|||||||
version: VERSION.to_string(),
|
version: VERSION.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
None => get_next_address_index(&db, &netconfig.name, &netconfig.address),
|
None => {
|
||||||
|
let next = get_next_address_index(&db, &netconfig.name, &netconfig.address);
|
||||||
|
next
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}; // lock released
|
}; // lock released
|
||||||
|
|
||||||
@@ -285,7 +288,8 @@ async fn echo_info(
|
|||||||
Ok(address) => address,
|
Ok(address) => address,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to derive address from xpub: {}", e);
|
error!("Failed to derive address from xpub: {}", e);
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest()
|
||||||
|
.body(format!("Failed to derive address: {}", e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,7 +299,7 @@ async fn echo_info(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_info (save phase)");
|
error!("DB mutex poisoned in echo_info (save phase)");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
save_new_address(&db, next_idx.0, &derived.0, &derived.1, &remote_addr);
|
save_new_address(&db, next_idx.0, &derived.0, &derived.1, &remote_addr);
|
||||||
@@ -318,30 +322,30 @@ async fn echo_info(
|
|||||||
debug!("echo info reply: {}", json_data);
|
debug!("echo info reply: {}", json_data);
|
||||||
HttpResponse::Ok().json(info)
|
HttpResponse::Ok().json(info)
|
||||||
}
|
}
|
||||||
Err(_err) => HttpResponse::InternalServerError().body("error"),
|
Err(err) => HttpResponse::InternalServerError().body(format!("error:{}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("error");
|
return HttpResponse::NotFound().body("Unknown network");
|
||||||
}
|
}
|
||||||
info!("echo stats!!! {}", data.cfg.expose_stats);
|
info!("echo stats!!! {}", data.cfg.expose_stats);
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
debug!("network disabled {}", param);
|
debug!("network disabled {}", param);
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("network disabled");
|
||||||
}
|
}
|
||||||
if !data.cfg.expose_stats {
|
if !data.cfg.expose_stats {
|
||||||
return HttpResponse::Forbidden().body("error");
|
return HttpResponse::Forbidden().body("Stats not exposed");
|
||||||
}
|
}
|
||||||
let mut stats: Vec<StatsResponse> = vec![];
|
let mut stats: Vec<StatsResponse> = vec![];
|
||||||
let db = match data.db.lock() {
|
let db = match data.db.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_stats");
|
error!("DB mutex poisoned in echo_stats");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut stmt = match db.prepare(
|
let mut stmt = match db.prepare(
|
||||||
@@ -350,12 +354,12 @@ async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl
|
|||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to prepare stats query: {}", e);
|
error!("Failed to prepare stats query: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = stmt.bind((1, Value::String(netconfig.name.clone()))) {
|
if let Err(e) = stmt.bind((1, Value::String(netconfig.name.clone()))) {
|
||||||
error!("Failed to bind chain in stats query: {}", e);
|
error!("Failed to bind chain in stats query: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
while let Ok(State::Row) = stmt.next() {
|
while let Ok(State::Row) = stmt.next() {
|
||||||
let report_date = stmt.read("report_date").unwrap_or("0".to_string());
|
let report_date = stmt.read("report_date").unwrap_or("0".to_string());
|
||||||
@@ -418,7 +422,7 @@ async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl
|
|||||||
debug!("echo info reply: {}", json_data);
|
debug!("echo info reply: {}", json_data);
|
||||||
HttpResponse::Ok().json(stats)
|
HttpResponse::Ok().json(stats)
|
||||||
}
|
}
|
||||||
Err(_err) => HttpResponse::InternalServerError().body("error"),
|
Err(err) => HttpResponse::InternalServerError().body(format!("error:{}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,33 +431,33 @@ async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
|||||||
let strbody = match std::str::from_utf8(&body) {
|
let strbody = match std::str::from_utf8(&body) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Invalid UTF-8 body");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!("{}", strbody);
|
info!("{}", strbody);
|
||||||
|
|
||||||
if strbody.is_empty() || strbody.len() != 64 || !strbody.chars().all(|c| c.is_ascii_hexdigit())
|
if strbody.is_empty() || strbody.len() != 64 || !strbody.chars().all(|c| c.is_ascii_hexdigit())
|
||||||
{
|
{
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Invalid txid");
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = match data.db.lock() {
|
let db = match data.db.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_search");
|
error!("DB mutex poisoned in echo_search");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut statement = match db.prepare("SELECT * FROM tbl_tx WHERE txid = ? LIMIT 1") {
|
let mut statement = match db.prepare("SELECT * FROM tbl_tx WHERE txid = ? LIMIT 1") {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to prepare statement: {}", e);
|
error!("Failed to prepare statement: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = statement.bind((1, strbody)) {
|
if let Err(e) = statement.bind((1, strbody)) {
|
||||||
error!("Failed to bind parameter: {}", e);
|
error!("Failed to bind parameter: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(State::Row) = statement.next() {
|
if let Ok(State::Row) = statement.next() {
|
||||||
@@ -499,14 +503,11 @@ async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
match serde_json::to_string(&response_data) {
|
match serde_json::to_string(&response_data) {
|
||||||
Ok(json_data) => {
|
Ok(json_data) => HttpResponse::Ok().json(json_data),
|
||||||
debug!("echo search reply: {}", json_data);
|
Err(_) => HttpResponse::BadRequest().body("Bad data received"),
|
||||||
HttpResponse::Ok().json(&response_data)
|
|
||||||
}
|
|
||||||
Err(_) => HttpResponse::BadRequest().body("error"),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::BadRequest().body("error")
|
HttpResponse::BadRequest().body("Bad data received")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,13 +524,13 @@ struct ParsedTx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse all transactions from the request body **without** needing the DB lock.
|
/// Parse all transactions from the request body **without** needing the DB lock.
|
||||||
/// Skips transactions that don't have a valid willexecutor output.
|
/// Returns `Ok(parsed_txs)` if at least one tx was valid, or `Err(HttpResponse)` for early failure.
|
||||||
fn parse_request_transactions(
|
fn parse_request_transactions(
|
||||||
strbody: &str,
|
strbody: &str,
|
||||||
_req_time: i64,
|
_req_time: i64,
|
||||||
netconfig: &NetConfig,
|
netconfig: &NetConfig,
|
||||||
known_addresses: &HashSet<String>,
|
known_addresses: &HashSet<String>,
|
||||||
) -> Vec<(ParsedTx, String, u64)> {
|
) -> Result<Vec<(ParsedTx, String, u64)>, HttpResponse> {
|
||||||
let mut result: Vec<(ParsedTx, String, u64)> = Vec::new();
|
let mut result: Vec<(ParsedTx, String, u64)> = Vec::new();
|
||||||
let mut union_tx = true;
|
let mut union_tx = true;
|
||||||
|
|
||||||
@@ -612,8 +613,8 @@ fn parse_request_transactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
trace!("willexecutor output not found for tx {}, skipping", txid);
|
error!("willexecutor output not found for tx {}", txid);
|
||||||
continue;
|
return Err(HttpResponse::BadRequest().body("Bad data received"));
|
||||||
}
|
}
|
||||||
if !union_tx {
|
if !union_tx {
|
||||||
// This is only used for SQL building later; we track it in the caller
|
// This is only used for SQL building later; we track it in the caller
|
||||||
@@ -635,7 +636,7 @@ fn parse_request_transactions(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_push(
|
async fn echo_push(
|
||||||
@@ -647,24 +648,24 @@ async fn echo_push(
|
|||||||
let strbody = match std::str::from_utf8(&body) {
|
let strbody = match std::str::from_utf8(&body) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Invalid UTF-8 body");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("error");
|
return HttpResponse::NotFound().body("Unknown network");
|
||||||
}
|
}
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
trace!("network not enabled {}", &netconfig.name);
|
trace!("network not enabled {}", &netconfig.name);
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Network not enabled");
|
||||||
}
|
}
|
||||||
let req_time = match Utc::now().timestamp_nanos_opt() {
|
let req_time = match Utc::now().timestamp_nanos_opt() {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
error!("Invalid timestamp");
|
error!("Invalid timestamp");
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Invalid timestamp");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -674,7 +675,7 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned acquiring addresses in echo_push");
|
error!("DB mutex poisoned acquiring addresses in echo_push");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if netconfig.xpub {
|
if netconfig.xpub {
|
||||||
@@ -682,7 +683,7 @@ async fn echo_push(
|
|||||||
Ok(addrs) => addrs,
|
Ok(addrs) => addrs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load addresses from xpub: {}", e);
|
error!("Failed to load addresses from xpub: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -691,9 +692,12 @@ async fn echo_push(
|
|||||||
}; // lock released here
|
}; // lock released here
|
||||||
|
|
||||||
// Parse all transactions (CPU-bound, no DB needed)
|
// Parse all transactions (CPU-bound, no DB needed)
|
||||||
let parsed = parse_request_transactions(strbody, req_time, netconfig, &known_addresses);
|
let parsed = match parse_request_transactions(strbody, req_time, netconfig, &known_addresses) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(resp) => return resp,
|
||||||
|
};
|
||||||
if parsed.is_empty() {
|
if parsed.is_empty() {
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::Ok().body("thx");
|
||||||
}
|
}
|
||||||
|
|
||||||
let all_txids: Vec<String> = parsed.iter().map(|(p, _, _)| p.txid.clone()).collect();
|
let all_txids: Vec<String> = parsed.iter().map(|(p, _, _)| p.txid.clone()).collect();
|
||||||
@@ -704,14 +708,14 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_push duplicate check");
|
error!("DB mutex poisoned in echo_push duplicate check");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match check_duplicate_txids(&db, &all_txids) {
|
match check_duplicate_txids(&db, &all_txids) {
|
||||||
Ok(dups) => dups,
|
Ok(dups) => dups,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Duplicate check failed: {}", e);
|
error!("Duplicate check failed: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("Database error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; // lock released here
|
}; // lock released here
|
||||||
@@ -727,7 +731,7 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_push insert phase");
|
error!("DB mutex poisoned in echo_push insert phase");
|
||||||
return HttpResponse::InternalServerError().body("error");
|
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -814,7 +818,7 @@ async fn echo_push(
|
|||||||
|
|
||||||
if let Err(err) = execute_insert(&db, sqltxs, ptx, sqlinps, pinps, sqlouts, pouts) {
|
if let Err(err) = execute_insert(&db, sqltxs, ptx, sqlinps, pinps, sqlouts, pouts) {
|
||||||
error!("execute_insert failed: {}", err);
|
error!("execute_insert failed: {}", err);
|
||||||
return HttpResponse::BadRequest().body("error");
|
return HttpResponse::BadRequest().body("Bad data received");
|
||||||
}
|
}
|
||||||
} // lock released
|
} // lock released
|
||||||
|
|
||||||
@@ -896,7 +900,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let db = match open_db(&cfg.db_file) {
|
let db = match open_db(&cfg.db_file) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(std::io::Error::other(e));
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -178,8 +178,8 @@ pub fn create_database(db: &Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
pub fn insert_xpub(db: &Connection, network: &str, xpub: &str) {
|
pub fn insert_xpub(db: &Connection, network: &String, xpub: &String) {
|
||||||
if !xpub.is_empty() {
|
if xpub != "" {
|
||||||
trace!("going to insert: {} xpub:{}", network, xpub);
|
trace!("going to insert: {} xpub:{}", network, xpub);
|
||||||
let mut stmt = match db.prepare("INSERT INTO tbl_xpub(network,xpub) VALUES(?, ?);") {
|
let mut stmt = match db.prepare("INSERT INTO tbl_xpub(network,xpub) VALUES(?, ?);") {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|||||||
32
src/xpub.rs
32
src/xpub.rs
@@ -11,7 +11,6 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Mainnet (BIP44/BIP49/BIP84)
|
// Mainnet (BIP44/BIP49/BIP84)
|
||||||
#[allow(dead_code)]
|
|
||||||
enum BS58Prefix {
|
enum BS58Prefix {
|
||||||
Xpub,
|
Xpub,
|
||||||
Ypub,
|
Ypub,
|
||||||
@@ -103,29 +102,44 @@ fn calc_checksum(desc: &str) -> Result<String, String> {
|
|||||||
Ok(checksum)
|
Ok(checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bitcoincore_descriptor(xpub: &str) -> String {
|
pub fn get_bitcoincore_descriptor(xpub: &String) -> String {
|
||||||
let fingerprint = match calculate_fingerprint(xpub) {
|
let fingerprint = match calculate_fingerprint(xpub) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut bip = 84;
|
||||||
|
let cpub = xpub.to_string();
|
||||||
|
match &xpub[0..4] {
|
||||||
|
"vpub" => {
|
||||||
|
bip = 84;
|
||||||
|
}
|
||||||
|
"zpub" => {
|
||||||
|
bip = 84;
|
||||||
|
}
|
||||||
|
&_ => {
|
||||||
|
bip = 84;
|
||||||
|
}
|
||||||
|
};
|
||||||
let xpub_converted = match convert_xpub(xpub) {
|
let xpub_converted = match convert_xpub(xpub) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
||||||
};
|
};
|
||||||
let descriptor = format!("wpkh([{}/84h/0h/0h]{}/0/*)", fingerprint, xpub_converted);
|
let descriptor = format!("wpkh([{}/84h/0h/0h]{}/0/*)", fingerprint, xpub_converted);
|
||||||
match calc_checksum(&descriptor) {
|
let descriptor = match calc_checksum(&descriptor) {
|
||||||
Ok(checksum) => {
|
Ok(checksum) => {
|
||||||
let clean_descriptor = descriptor.split('#').next().unwrap_or(&descriptor);
|
let clean_descriptor = descriptor.split('#').next().unwrap_or(&descriptor);
|
||||||
format!("{}#{}", clean_descriptor, checksum)
|
format!("{}#{}", clean_descriptor, checksum)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {}", err);
|
eprintln!("Error: {}", err);
|
||||||
String::new()
|
"".to_string()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
descriptor
|
||||||
|
//format!("{}#{}",descriptor,checksum)
|
||||||
}
|
}
|
||||||
fn convert_xpub(xpub: &str) -> Result<String, String> {
|
fn convert_xpub(xpub: &String) -> Result<String, String> {
|
||||||
if xpub.len() >= 4 && (&xpub[0..4] == "xpub" || &xpub[0..4] == "ypub" || &xpub[0..4] == "zpub")
|
if xpub.len() >= 4 && (&xpub[0..4] == "xpub" || &xpub[0..4] == "ypub" || &xpub[0..4] == "zpub")
|
||||||
{
|
{
|
||||||
convert_to(xpub, BS58Prefix::Xpub)
|
convert_to(xpub, BS58Prefix::Xpub)
|
||||||
@@ -151,7 +165,7 @@ fn base58check_decode(s: &str) -> Result<Vec<u8>, String> {
|
|||||||
return Err("Data troppo corta".to_string());
|
return Err("Data troppo corta".to_string());
|
||||||
}
|
}
|
||||||
let (payload, checksum) = data.split_at(data.len() - 4);
|
let (payload, checksum) = data.split_at(data.len() - 4);
|
||||||
let hash = Sha256::digest(Sha256::digest(payload));
|
let hash = Sha256::digest(&Sha256::digest(payload));
|
||||||
if hash[0..4] != checksum[..] {
|
if hash[0..4] != checksum[..] {
|
||||||
return Err("Checksum invalido".to_string());
|
return Err("Checksum invalido".to_string());
|
||||||
}
|
}
|
||||||
@@ -159,7 +173,7 @@ fn base58check_decode(s: &str) -> Result<Vec<u8>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn base58check_encode(data: &[u8]) -> String {
|
fn base58check_encode(data: &[u8]) -> String {
|
||||||
let checksum = &Sha256::digest(Sha256::digest(data))[0..4];
|
let checksum = &Sha256::digest(&Sha256::digest(data))[0..4];
|
||||||
let full = [data, checksum].concat();
|
let full = [data, checksum].concat();
|
||||||
bs58::encode(full).into_string()
|
bs58::encode(full).into_string()
|
||||||
}
|
}
|
||||||
@@ -191,7 +205,7 @@ pub fn new_address_from_xpub(
|
|||||||
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
||||||
let xpub = Xpub::from_str(&convert_to(zpub, BS58Prefix::Xpub)?)?;
|
let xpub = Xpub::from_str(&convert_to(zpub, BS58Prefix::Xpub)?)?;
|
||||||
let path = format!("m/0/{}", index);
|
let path = format!("m/0/{}", index);
|
||||||
let derivation_path = DerivationPath::from_str(path.as_str())?;
|
let derivation_path = DerivationPath::from_str(&path.as_str())?;
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let derived_xpub = xpub.derive_pub(&secp, &derivation_path)?;
|
let derived_xpub = xpub.derive_pub(&secp, &derivation_path)?;
|
||||||
let public_key = derived_xpub.public_key;
|
let public_key = derived_xpub.public_key;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use bal_server::db::open_db;
|
use bal_server::db::open_db;
|
||||||
use sqlite::State;
|
use sqlite::State;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_open_db_blocks_traversal() {
|
fn test_open_db_blocks_traversal() {
|
||||||
@@ -73,7 +74,7 @@ fn test_open_db_rejects_symlink() {
|
|||||||
let _ = fs::remove_file(real);
|
let _ = fs::remove_file(real);
|
||||||
let _ = fs::remove_file(link);
|
let _ = fs::remove_file(link);
|
||||||
fs::File::create(real).unwrap();
|
fs::File::create(real).unwrap();
|
||||||
std::os::unix::fs::symlink(real, link).unwrap();
|
fs::soft_link(real, link).unwrap();
|
||||||
|
|
||||||
let res = open_db(link);
|
let res = open_db(link);
|
||||||
assert!(res.is_err(), "Symlink DB path should be rejected");
|
assert!(res.is_err(), "Symlink DB path should be rejected");
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ fn test_db_null_unwrap_or() {
|
|||||||
|
|
||||||
let mut found_value = None;
|
let mut found_value = None;
|
||||||
let _ = db.iterate("SELECT * FROM test_stats;", |pairs| {
|
let _ = db.iterate("SELECT * FROM test_stats;", |pairs| {
|
||||||
let row: HashMap<_, _> = pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect();
|
let row: HashMap<_, _> = pairs
|
||||||
let totals = row["totals"].unwrap_or("0").to_string();
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.map(|s| s)))
|
||||||
|
.collect();
|
||||||
|
let totals = row["totals"].clone().unwrap_or("0").to_string();
|
||||||
found_value = Some(totals);
|
found_value = Some(totals);
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gitignore_protection_env() {
|
fn test_gitignore_protection_env() {
|
||||||
@@ -22,20 +23,30 @@ fn test_gitignore_protection_env() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for pattern in required_patterns {
|
for pattern in required_patterns {
|
||||||
let has_wildcard = gitignore.contains("*.env.local") || gitignore.contains(".env.local");
|
let has_exact = gitignore.contains(&pattern);
|
||||||
|
let has_wildcard = gitignore.contains(&format!("*.env.local"))
|
||||||
|
|| gitignore.contains(&format!(".env.local"));
|
||||||
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
|
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
|
||||||
|
|
||||||
|
// For .env.local, either .env.local or *.env.local is acceptable
|
||||||
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
|
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
|
||||||
if is_env_local {
|
if is_env_local {
|
||||||
assert!(
|
assert!(
|
||||||
has_wildcard,
|
has_wildcard,
|
||||||
".gitignore must contain pattern '*.env.local' or '.env.local' to protect secrets",
|
".gitignore must contain pattern '*.env.local' or '.env.local' to protect secrets",
|
||||||
);
|
);
|
||||||
} else if pattern == ".env.production"
|
} else if pattern == ".env.production" || pattern == ".env.secret" {
|
||||||
|| pattern == ".env.secret"
|
assert!(
|
||||||
|| pattern == "*.env"
|
gitignore.contains(pattern),
|
||||||
|| pattern == ".env"
|
".gitignore must contain pattern '{}' to protect secrets",
|
||||||
{
|
pattern
|
||||||
|
);
|
||||||
|
} else if pattern == "*.env" {
|
||||||
|
assert!(
|
||||||
|
has_env,
|
||||||
|
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
||||||
|
);
|
||||||
|
} else if pattern == ".env" {
|
||||||
assert!(
|
assert!(
|
||||||
has_env,
|
has_env,
|
||||||
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
||||||
@@ -54,6 +65,7 @@ fn test_gitignore_protection_env() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_private_key_in_git() {
|
fn test_no_private_key_in_git() {
|
||||||
|
// Check that .gitignore includes private_key.pem
|
||||||
let gitignore = match fs::read_to_string(".gitignore") {
|
let gitignore = match fs::read_to_string(".gitignore") {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -76,17 +88,20 @@ fn test_no_private_key_in_git() {
|
|||||||
".gitignore must block chiave_privata.key"
|
".gitignore must block chiave_privata.key"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check that no private key files are tracked by git
|
||||||
let output = std::process::Command::new("git")
|
let output = std::process::Command::new("git")
|
||||||
.args(["ls-files", "*.pem", "*.key"])
|
.args(&["ls-files", "*.pem", "*.key"])
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run git ls-files");
|
.expect("Failed to run git ls-files");
|
||||||
|
|
||||||
let tracked_keys = String::from_utf8(output.stdout).unwrap();
|
let tracked_keys = String::from_utf8(output.stdout).unwrap();
|
||||||
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
|
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
|
||||||
|
|
||||||
|
// Only non-empty entries and only public_key.pem should be tracked
|
||||||
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
|
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
|
||||||
if !tracked.contains("public_key.pem") {
|
if !tracked.contains("public_key.pem") {
|
||||||
panic!(
|
assert!(
|
||||||
|
false,
|
||||||
"Private key file is tracked by git: {}. Remove it with git rm --cached",
|
"Private key file is tracked by git: {}. Remove it with git rm --cached",
|
||||||
tracked
|
tracked
|
||||||
);
|
);
|
||||||
@@ -98,34 +113,38 @@ fn test_no_private_key_in_git() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_token_in_source_files() {
|
fn test_no_token_in_source_files() {
|
||||||
|
// Scan source files for hardcoded tokens
|
||||||
let mut found_issues = Vec::new();
|
let mut found_issues = Vec::new();
|
||||||
|
|
||||||
|
// Scan .sh files for hardcoded 40-char hex strings
|
||||||
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
|
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(ext) = path.extension()
|
if let Some(ext) = path.extension() {
|
||||||
&& ext == "sh"
|
if ext == "sh" {
|
||||||
{
|
|
||||||
let content = fs::read_to_string(&path).unwrap();
|
let content = fs::read_to_string(&path).unwrap();
|
||||||
for (line_num, line) in content.lines().enumerate() {
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
if line.trim().starts_with('#')
|
// Skip comments and example/template files
|
||||||
|
if line.trim().starts_with("#")
|
||||||
|| line.to_lowercase().contains("example")
|
|| line.to_lowercase().contains("example")
|
||||||
|| line.to_lowercase().contains("template")
|
|| line.to_lowercase().contains("template")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Check for 40-64 hex chars that could be API tokens (not in .env.example comments)
|
||||||
if line.trim().len() >= 40 {
|
if line.trim().len() >= 40 {
|
||||||
let hex_chars: Vec<_> = line
|
let hex_chars = line
|
||||||
.trim()
|
.trim()
|
||||||
.chars()
|
.chars()
|
||||||
.filter(|c| c.is_ascii_hexdigit())
|
.filter(|c| c.is_ascii_hexdigit())
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
if (40..=64).contains(&hex_chars.len())
|
if hex_chars.len() >= 40 && hex_chars.len() <= 64 {
|
||||||
&& (line.to_lowercase().contains("token")
|
// Check if it looks like it's part of a TOKEN assignment
|
||||||
|
if line.to_lowercase().contains("token")
|
||||||
|| line.to_lowercase().contains("api")
|
|| line.to_lowercase().contains("api")
|
||||||
|| line.to_lowercase().contains("secret"))
|
|| line.to_lowercase().contains("secret")
|
||||||
{
|
{
|
||||||
found_issues.push(format!(
|
found_issues.push(format!(
|
||||||
"Potential hardcoded token in {}: line {}: {}",
|
"Potential hardcoded token in {}: line {}: {}",
|
||||||
@@ -138,13 +157,16 @@ fn test_no_token_in_source_files() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !found_issues.is_empty() {
|
if !found_issues.is_empty() {
|
||||||
println!("FAIL: Found potential hardcoded tokens:");
|
println!("FAIL: Found potential hardcoded tokens:");
|
||||||
for issue in &found_issues {
|
for issue in &found_issues {
|
||||||
println!(" {}", issue);
|
println!(" {}", issue);
|
||||||
}
|
}
|
||||||
panic!(
|
assert!(
|
||||||
|
false,
|
||||||
"Found potential hardcoded tokens in shell scripts: {:?}",
|
"Found potential hardcoded tokens in shell scripts: {:?}",
|
||||||
found_issues
|
found_issues
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user