mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 13:19:26 -07:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e27da68b5d | ||
|
|
2a68c99897 | ||
|
|
987d95c23e | ||
|
|
9ef6b43dac | ||
|
|
ffc42f6ffd | ||
|
|
2781b3c7ed | ||
|
|
fd63210bf9 | ||
|
|
a271c4ddf4 | ||
|
|
bef6b51e28 | ||
|
|
781d07230c | ||
|
|
1f171521e4 | ||
|
|
5b2cf3cec4 | ||
|
|
62e8d4c40f | ||
|
|
72c19e0f04 | ||
|
|
9b52f46c1a | ||
|
|
51d4e86b3a | ||
|
|
33fafd4707 | ||
|
|
6e4cbac4b1 | ||
|
|
b453c92d6a | ||
|
|
733c8b227d | ||
|
|
b43217ef35 | ||
|
|
40a0dec361 | ||
|
|
b2d5ed356f | ||
|
|
6033757ddb | ||
|
|
6b4f98183e | ||
|
|
bd2329d6cc | ||
|
|
d1311e0ba3 | ||
|
|
75cf03d638 | ||
|
|
be15035ad4 | ||
|
|
a3d0d8f4f9 | ||
|
|
2c30218743 | ||
|
|
eb65214989 | ||
|
|
8d86aeb591 | ||
|
|
23cef7349e | ||
|
|
07e0115192 | ||
|
|
82b53c6187 | ||
|
|
883175aa59 | ||
|
|
bd52718ea7 | ||
|
|
d607c63cc8 | ||
|
|
9e08e662ff | ||
|
|
08920e02b8 | ||
|
|
7e2df91702 | ||
|
|
262f583355 | ||
|
|
9ae1563286 | ||
|
|
2bd6efa503 | ||
|
|
e06769158b | ||
|
|
b341ef2d1e | ||
|
|
3a807f48b2 | ||
|
|
bc3f0bf515 | ||
|
|
d3290a2c2d | ||
|
|
579c2c1f3f |
@@ -5,6 +5,10 @@ build-daemon-firmware = "build -p rayhunter-daemon --bin rayhunter-daemon --targ
|
||||
# Build the daemon with "firmware-devel" profile and "rustcrypto" backend.
|
||||
# Works with just the Rust toolchain, and is medium-slow to build. Binaries are slightly larger.
|
||||
build-daemon-firmware-devel = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware-devel"
|
||||
# Build rootshell for firmware
|
||||
build-rootshell-firmware = "build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware"
|
||||
# Build rootshell for development
|
||||
build-rootshell-firmware-devel = "build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware-devel"
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
linker = "rust-lld"
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,7 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frequently Asked Questions
|
||||
url: https://efforg.github.io/rayhunter/faq.html
|
||||
- name: Questions and community
|
||||
url: https://efforg.github.io/rayhunter/support-feedback-community.html
|
||||
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help on GitHub discussions or Mattermost
|
||||
|
||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@@ -1,7 +1,8 @@
|
||||
## Pull Request Checklist
|
||||
|
||||
- [ ] The Rayhunter team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
|
||||
- [ ] The Rayhunter team has recently expressed interest in reviewing a PR for this.
|
||||
- If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
|
||||
- [ ] Added or updated any documentation as needed to support the changes in this PR.
|
||||
- [ ] Code has been linted and run through `cargo fmt`
|
||||
- [ ] If any new functionality has been added, unit tests were also added
|
||||
- [ ] [./CONTRIBUTING.md](../CONTRIBUTING.md) has been read
|
||||
- [ ] Code has been linted and run through `cargo fmt`.
|
||||
- [ ] If any new functionality has been added, unit tests were also added.
|
||||
- [ ] [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md) has been read.
|
||||
|
||||
66
.github/workflows/main.yml
vendored
66
.github/workflows/main.yml
vendored
@@ -20,13 +20,14 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
code_changed: ${{ steps.files_changed.outputs.code_count }}
|
||||
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
|
||||
web_changed: ${{ steps.files_changed.outputs.web_count }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
||||
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
|
||||
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count }}
|
||||
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||
code_changed: ${{ steps.files_changed.outputs.code_count != '0' }}
|
||||
daemon_changed: ${{ steps.files_changed.outputs.daemon_count != '0' }}
|
||||
daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
|
||||
web_changed: ${{ steps.files_changed.outputs.web_count != '0' }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' }}
|
||||
installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }}
|
||||
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }}
|
||||
rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -37,14 +38,18 @@ jobs:
|
||||
run: |
|
||||
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
|
||||
|
||||
# If we are on main, or if these workflow files are being changed, run everything
|
||||
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo
|
||||
# If we are on main, if workflow/cargo config files changed, or if
|
||||
# the latest commit message contains "#build-all", run everything.
|
||||
# Use #build-all in a commit message to force a full build on a PR
|
||||
# branch (useful for testing release builds without merging to main).
|
||||
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo || git log -1 --format='%s %b' | grep -qF '#build-all'
|
||||
then
|
||||
echo "building everything"
|
||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo daemon_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo web_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo docs_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo installer_build=forced >> "$GITHUB_OUTPUT"
|
||||
echo installer_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo installer_gui_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
|
||||
@@ -53,15 +58,25 @@ jobs:
|
||||
echo "daemon_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^lib -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "web_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon/web | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "docs_count=$(git diff --name-only $lcommit...HEAD | grep -e ^book.toml -e ^doc | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer/ | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "installer_gui_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer-gui | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer/ | wc -l)
|
||||
installer_gui_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer-gui | wc -l)
|
||||
|
||||
if [ $installer_count != "0" ] || [ $installer_gui_count != "0" ]; then
|
||||
echo "installer_build=1" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "installer_build=0" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
echo "installer_count=$installer_count" >> "$GITHUB_OUTPUT"
|
||||
echo "installer_gui_count=$installer_gui_count" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
mdbook_test:
|
||||
name: Test mdBook Documentation builds
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.docs_changed != '0'
|
||||
if: needs.files_changed.outputs.docs_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -106,7 +121,7 @@ jobs:
|
||||
|
||||
check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.code_changed != '0'
|
||||
if: needs.files_changed.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -140,7 +155,7 @@ jobs:
|
||||
# installer-gui isn't one of the default workspace packages
|
||||
# 2) avoid slowing down development on changes unrelated to the GUI installer
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_gui_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
# we run this on macos simply because no additional OS packages need to be
|
||||
# installed
|
||||
runs-on: macos-latest
|
||||
@@ -164,7 +179,7 @@ jobs:
|
||||
|
||||
test_daemon_frontend:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.web_changed != '0'
|
||||
if: needs.files_changed.outputs.web_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -182,7 +197,7 @@ jobs:
|
||||
|
||||
test_installer_frontend:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_gui_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -199,7 +214,7 @@ jobs:
|
||||
|
||||
windows_installer_check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_changed == 'true'
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -220,7 +235,7 @@ jobs:
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
|
||||
|
||||
build_rayhunter_check:
|
||||
if: needs.files_changed.outputs.daemon_changed != '0'
|
||||
if: needs.files_changed.outputs.daemon_changed == 'true'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -266,7 +281,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rootshell:
|
||||
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
|
||||
if: needs.files_changed.outputs.rootshell_needed == 'true'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -290,10 +305,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rayhunter:
|
||||
# build_rust_installer needs this step. so when installer_changed, we need
|
||||
# to build this step too. if we skip this step because only the installer
|
||||
# changed, the build_rust_installer step will be skipped too.
|
||||
if: needs.files_changed.outputs.daemon_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
|
||||
if: needs.files_changed.outputs.daemon_needed == 'true'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -333,7 +345,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rust_installer:
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -381,7 +393,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_linux:
|
||||
if: needs.files_changed.outputs.installer_gui_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -437,7 +449,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_macos:
|
||||
if: needs.files_changed.outputs.installer_gui_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -480,7 +492,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_windows:
|
||||
if: needs.files_changed.outputs.installer_gui_changed != '0'
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
89
Cargo.lock
generated
89
Cargo.lock
generated
@@ -709,9 +709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -935,16 +935,6 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1339,12 +1329,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2735,7 +2725,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
@@ -2763,7 +2753,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer-gui"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"installer",
|
||||
@@ -3423,11 +3413,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
@@ -3441,9 +3430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
@@ -3513,21 +3502,12 @@ version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||
dependencies = [
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nusb"
|
||||
version = "0.1.14"
|
||||
@@ -4691,12 +4671,13 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"deku 0.20.2",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4713,28 +4694,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-check"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
"log",
|
||||
"pcap-file-tokio",
|
||||
"rayhunter",
|
||||
"simple_logger",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"axum",
|
||||
"chrono",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"futures-macro",
|
||||
"image",
|
||||
@@ -4916,16 +4895,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
@@ -5435,18 +5414,6 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_logger"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"log",
|
||||
"time",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
@@ -5984,7 +5951,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.8.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
@@ -6090,32 +6057,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,5 +10,4 @@ log = "0.4.20"
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
simple_logger = "5.0.0"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
@@ -177,14 +177,7 @@ async fn main() {
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
simple_logger::SimpleLogger::new()
|
||||
.with_colors(true)
|
||||
.without_timestamps()
|
||||
.with_level(level)
|
||||
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
|
||||
.with_module_level("asn1_codecs", log::LevelFilter::Error)
|
||||
.init()
|
||||
.unwrap();
|
||||
rayhunter::init_logging(level);
|
||||
|
||||
let harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
info!("Analyzers:");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
@@ -18,7 +18,6 @@ axum = { version = "0.8", default-features = false, features = ["http1", "tokio"
|
||||
thiserror = "1.0.52"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
|
||||
futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use log::{error, info};
|
||||
use log::{info, warn};
|
||||
use rayhunter::Device;
|
||||
use serde::Serialize;
|
||||
use tokio::select;
|
||||
@@ -66,11 +66,11 @@ pub fn run_battery_notification_worker(
|
||||
// Don't send a notification initially if the device starts at a low battery level.
|
||||
let mut triggered = match get_battery_status(&device).await {
|
||||
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
|
||||
info!("Battery level function not supported for device");
|
||||
false
|
||||
info!("Battery status not supported for this device, disabling battery notifications");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get battery status: {e}");
|
||||
warn!("Failed to get battery status: {e}");
|
||||
true
|
||||
}
|
||||
Ok(status) => status.level <= LOW_BATTERY_LEVEL,
|
||||
@@ -83,8 +83,12 @@ pub fn run_battery_notification_worker(
|
||||
}
|
||||
|
||||
let status = match get_battery_status(&device).await {
|
||||
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
|
||||
info!("Battery status not supported for this device, disabling battery notifications");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get battery status: {e}");
|
||||
warn!("Failed to get battery status: {e}");
|
||||
continue;
|
||||
}
|
||||
Ok(status) => status,
|
||||
|
||||
@@ -77,6 +77,7 @@ impl DiagTask {
|
||||
|
||||
/// Start recording
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
self.max_type_seen = EventType::Informational;
|
||||
let (qmdl_file, analysis_file) = qmdl_store
|
||||
.new_entry()
|
||||
.await
|
||||
|
||||
@@ -215,9 +215,13 @@ pub fn update_ui(
|
||||
Err(e) => error!("error receiving framebuffer update message: {e}"),
|
||||
}
|
||||
|
||||
let mut status_bar_height = 2;
|
||||
match display_level {
|
||||
2 => fb.draw_gif(img.unwrap()).await,
|
||||
3 => fb.draw_img(img.unwrap()).await,
|
||||
4 => {
|
||||
status_bar_height = fb.dimensions().height;
|
||||
}
|
||||
128 => {
|
||||
fb.draw_line(Color::Cyan, 128).await;
|
||||
fb.draw_line(Color::Pink, 102).await;
|
||||
@@ -225,12 +229,13 @@ pub fn update_ui(
|
||||
fb.draw_line(Color::Pink, 50).await;
|
||||
fb.draw_line(Color::Cyan, 25).await;
|
||||
}
|
||||
// this branch id for ui_level 1, which is also the default if an
|
||||
// this branch is for ui_level 1, which is also the default if an
|
||||
// unknown value is used
|
||||
_ => {}
|
||||
};
|
||||
let (color, pattern) = display_style;
|
||||
fb.draw_patterned_line(color, 2, pattern).await;
|
||||
fb.draw_patterned_line(color, status_bar_height, pattern)
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_millis(REFRESH_RATE)).await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,7 +22,8 @@ use crate::notifications::{NotificationService, run_notification_worker};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_zip, serve_static, set_config,
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_zip, serve_static,
|
||||
set_config, set_time_offset, test_notification,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
|
||||
@@ -68,6 +69,9 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/analysis/{name}", post(start_analysis))
|
||||
.route("/api/config", get(get_config))
|
||||
.route("/api/config", post(set_config))
|
||||
.route("/api/test-notification", post(test_notification))
|
||||
.route("/api/time", get(get_time))
|
||||
.route("/api/time-offset", post(set_time_offset))
|
||||
.route("/api/debug/display-state", post(debug_set_display_state))
|
||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||
.route("/{*path}", get(serve_static))
|
||||
@@ -167,7 +171,7 @@ fn run_shutdown_thread(
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), RayhunterError> {
|
||||
env_logger::init();
|
||||
rayhunter::init_logging(log::LevelFilter::Info);
|
||||
|
||||
#[cfg(feature = "rustcrypto-tls")]
|
||||
{
|
||||
@@ -203,6 +207,10 @@ async fn run_with_config(
|
||||
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||
let restart_token = CancellationToken::new();
|
||||
let shutdown_token = restart_token.child_token();
|
||||
// Ensure shutdown_token is cancelled when this function exits for any
|
||||
// reason (e.g. diag device init failure), so all spawned tasks get
|
||||
// signaled to stop.
|
||||
let _shutdown_guard = shutdown_token.clone().drop_guard();
|
||||
|
||||
let notification_service = NotificationService::new(config.ntfy_url.clone());
|
||||
|
||||
|
||||
@@ -6,9 +6,18 @@ use std::{
|
||||
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc::{self, error::TryRecvError};
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NotificationError {
|
||||
#[error("HTTP request failed: {0}")]
|
||||
RequestFailed(#[from] reqwest::Error),
|
||||
#[error("Server returned error status: {0}")]
|
||||
HttpError(reqwest::StatusCode),
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum NotificationType {
|
||||
Warning,
|
||||
@@ -60,6 +69,21 @@ impl NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a notification message to the specified URL.
|
||||
pub async fn send_notification(
|
||||
http_client: &reqwest::Client,
|
||||
url: &str,
|
||||
message: String,
|
||||
) -> Result<(), NotificationError> {
|
||||
let response = http_client.post(url).body(message).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NotificationError::HttpError(response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_notification_worker(
|
||||
task_tracker: &TaskTracker,
|
||||
mut notification_service: NotificationService,
|
||||
@@ -125,24 +149,15 @@ pub fn run_notification_worker(
|
||||
}
|
||||
}
|
||||
|
||||
match http_client
|
||||
.post(&url)
|
||||
.body(notification.message.clone())
|
||||
.send()
|
||||
.await
|
||||
match send_notification(&http_client, &url, notification.message.clone()).await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
notification.last_sent = Some(Instant::now());
|
||||
notification.failed_since_last_success = 0;
|
||||
notification.needs_sending = false;
|
||||
} else {
|
||||
notification.failed_since_last_success += 1;
|
||||
notification.last_attempt = Some(Instant::now());
|
||||
}
|
||||
Ok(()) => {
|
||||
notification.last_sent = Some(Instant::now());
|
||||
notification.failed_since_last_success = 0;
|
||||
notification.needs_sending = false;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to send notification to ntfy: {e}");
|
||||
error!("Failed to send notification: {e}");
|
||||
notification.failed_since_last_success += 1;
|
||||
notification.last_attempt = Some(Instant::now());
|
||||
}
|
||||
@@ -162,3 +177,205 @@ pub fn run_notification_worker(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{Router, body::Bytes, extract::State, routing::post};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestServerState {
|
||||
received_messages: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
async fn capture_notification(
|
||||
State(state): State<TestServerState>,
|
||||
body: Bytes,
|
||||
) -> &'static str {
|
||||
let message = String::from_utf8_lossy(&body).to_string();
|
||||
state.received_messages.lock().await.push(message);
|
||||
"OK"
|
||||
}
|
||||
|
||||
async fn setup_test_server() -> (Arc<Mutex<Vec<String>>>, String) {
|
||||
#[cfg(feature = "rustcrypto-tls")]
|
||||
{
|
||||
let _ = rustls_rustcrypto::provider().install_default();
|
||||
}
|
||||
|
||||
let received_messages = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_state = TestServerState {
|
||||
received_messages: received_messages.clone(),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", post(capture_notification))
|
||||
.with_state(test_state);
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let url = format!("http://{}", addr);
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
(received_messages, url)
|
||||
}
|
||||
|
||||
async fn cleanup_worker(sender: mpsc::Sender<Notification>, tracker: TaskTracker) {
|
||||
drop(sender);
|
||||
tracker.close();
|
||||
tracker.wait().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notification_worker_sends_message() {
|
||||
let (received_messages, url) = setup_test_server().await;
|
||||
|
||||
let task_tracker = TaskTracker::new();
|
||||
let notification_service = NotificationService::new(Some(url));
|
||||
let notification_sender = notification_service.new_handler();
|
||||
|
||||
run_notification_worker(
|
||||
&task_tracker,
|
||||
notification_service,
|
||||
vec![NotificationType::Warning],
|
||||
);
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
"test warning message".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
let messages = received_messages.lock().await;
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0], "test warning message");
|
||||
drop(messages);
|
||||
|
||||
cleanup_worker(notification_sender, task_tracker).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notification_worker_filters_disabled_types() {
|
||||
let (received_messages, url) = setup_test_server().await;
|
||||
|
||||
let task_tracker = TaskTracker::new();
|
||||
let notification_service = NotificationService::new(Some(url));
|
||||
let notification_sender = notification_service.new_handler();
|
||||
|
||||
run_notification_worker(
|
||||
&task_tracker,
|
||||
notification_service,
|
||||
vec![NotificationType::Warning],
|
||||
);
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
"test warning".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::LowBattery,
|
||||
"test low battery".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
let messages = received_messages.lock().await;
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0], "test warning");
|
||||
drop(messages);
|
||||
|
||||
cleanup_worker(notification_sender, task_tracker).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notification_worker_sends_enabled_types() {
|
||||
let (received_messages, url) = setup_test_server().await;
|
||||
|
||||
let task_tracker = TaskTracker::new();
|
||||
let notification_service = NotificationService::new(Some(url));
|
||||
let notification_sender = notification_service.new_handler();
|
||||
|
||||
run_notification_worker(
|
||||
&task_tracker,
|
||||
notification_service,
|
||||
vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
);
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
"test warning".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::LowBattery,
|
||||
"test low battery".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
let messages = received_messages.lock().await;
|
||||
assert_eq!(messages.len(), 2);
|
||||
// these are interchangeable, ordering not guaranteed
|
||||
assert!(messages.contains(&"test warning".to_string()));
|
||||
assert!(messages.contains(&"test low battery".to_string()));
|
||||
drop(messages);
|
||||
|
||||
cleanup_worker(notification_sender, task_tracker).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notification_worker_with_no_url() {
|
||||
let task_tracker = TaskTracker::new();
|
||||
let notification_service = NotificationService::new(None);
|
||||
let notification_sender = notification_service.new_handler();
|
||||
|
||||
run_notification_worker(
|
||||
&task_tracker,
|
||||
notification_service,
|
||||
vec![NotificationType::Warning],
|
||||
);
|
||||
|
||||
notification_sender
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
"test warning".to_string(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
cleanup_worker(notification_sender, task_tracker).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub struct ManifestEntry {
|
||||
|
||||
impl ManifestEntry {
|
||||
fn new() -> Self {
|
||||
let now = Local::now();
|
||||
let now = rayhunter::clock::get_adjusted_now();
|
||||
let metadata = RuntimeMetadata::new();
|
||||
ManifestEntry {
|
||||
name: format!("{}", now.timestamp()),
|
||||
@@ -300,7 +300,8 @@ impl RecordingStore {
|
||||
size_bytes: usize,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
||||
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
||||
self.manifest.entries[entry_index].last_message_time =
|
||||
Some(rayhunter::clock::get_adjusted_now());
|
||||
self.write_manifest().await
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ use axum::extract::State;
|
||||
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use axum::http::{HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use chrono::{DateTime, Local};
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::write;
|
||||
use tokio::io::{AsyncReadExt, copy, duplex};
|
||||
@@ -136,6 +138,76 @@ pub async fn set_config(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn test_notification(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
let url = state.config.ntfy_url.as_ref().ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"No notification URL configured".to_string(),
|
||||
))?;
|
||||
|
||||
if url.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Notification URL is empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
let message = "Test notification from Rayhunter".to_string();
|
||||
|
||||
crate::notifications::send_notification(&http_client, url, message)
|
||||
.await
|
||||
.map(|()| {
|
||||
(
|
||||
StatusCode::OK,
|
||||
"Test notification sent successfully".to_string(),
|
||||
)
|
||||
})
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to send test notification: {e}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Response for GET /api/time
|
||||
#[derive(Serialize)]
|
||||
pub struct TimeResponse {
|
||||
/// The raw system time (without clock offset)
|
||||
pub system_time: DateTime<Local>,
|
||||
/// The adjusted time (system time + offset)
|
||||
pub adjusted_time: DateTime<Local>,
|
||||
/// The current offset in seconds
|
||||
pub offset_seconds: i64,
|
||||
}
|
||||
|
||||
/// Request for POST /api/time-offset
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetTimeOffsetRequest {
|
||||
/// The offset to set, in seconds
|
||||
pub offset_seconds: i64,
|
||||
}
|
||||
|
||||
pub async fn get_time() -> Json<TimeResponse> {
|
||||
let system_time = Local::now();
|
||||
let adjusted_time = rayhunter::clock::get_adjusted_now();
|
||||
let offset_seconds = adjusted_time
|
||||
.signed_duration_since(system_time)
|
||||
.num_seconds();
|
||||
Json(TimeResponse {
|
||||
system_time,
|
||||
adjusted_time,
|
||||
offset_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_time_offset(Json(req): Json<SetTimeOffsetRequest>) -> StatusCode {
|
||||
rayhunter::clock::set_offset(chrono::TimeDelta::seconds(req.offset_seconds));
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
pub async fn get_zip(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(entry_name): Path<String>,
|
||||
|
||||
@@ -37,6 +37,17 @@ export default ts.config(
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'function',
|
||||
format: ['snake_case'],
|
||||
},
|
||||
{
|
||||
selector: 'method',
|
||||
format: ['snake_case'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
49
daemon/web/package-lock.json
generated
49
daemon/web/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
@@ -1167,18 +1167,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.46.2.tgz",
|
||||
"integrity": "sha512-bGs473Gj4TwFf7dw6ZUwQI0ayaDb83E7G06QnYeNQC2DmAaktgFU2uB0tSfZVhpHqYH4o8GsLBkG3ZjThtmsIA==",
|
||||
"version": "2.49.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.5.tgz",
|
||||
"integrity": "sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.3.2",
|
||||
"devalue": "^5.6.2",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -1197,11 +1198,15 @@
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1211,6 +1216,7 @@
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
@@ -1299,6 +1305,7 @@
|
||||
"integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
@@ -1349,6 +1356,7 @@
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
@@ -1682,6 +1690,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1935,6 +1944,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -2191,9 +2201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
|
||||
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2310,6 +2320,7 @@
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3021,6 +3032,7 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -3494,6 +3506,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3541,6 +3554,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3741,6 +3755,7 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -4225,6 +4240,7 @@
|
||||
"integrity": "sha512-Q3gqCGIgl4r0CR7OaWYjVo22nqFmLLSfn1MiWNFaITamvqhGBD3kyqk51EKuO4Nd1z8QliO5KIz7a2Ka9Rxilw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -4649,6 +4665,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4742,6 +4759,7 @@
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -5065,21 +5083,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
<p class="text-lg underline">Unparsed Messages</p>
|
||||
<p>
|
||||
These are due to a limitation or bug in Rayhunter's parser, and aren't usually a
|
||||
problem.
|
||||
problem. We'll not accept bug reports about them unless something else is going wrong
|
||||
(such as false-positives or definite false-negatives)
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-auto text-left">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
onclick,
|
||||
ariaLabel,
|
||||
errorMessage,
|
||||
jsonBody,
|
||||
}: {
|
||||
url: string;
|
||||
method?: string;
|
||||
@@ -23,6 +24,7 @@
|
||||
onclick?: () => void | Promise<void>;
|
||||
ariaLabel?: string;
|
||||
errorMessage?: string;
|
||||
jsonBody?: unknown;
|
||||
} = $props();
|
||||
|
||||
let is_requesting = $state(false);
|
||||
@@ -43,7 +45,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
async function handleClick() {
|
||||
async function handle_click() {
|
||||
if (is_disabled) return;
|
||||
|
||||
is_requesting = true;
|
||||
@@ -51,7 +53,8 @@
|
||||
await user_action_req(
|
||||
method,
|
||||
url,
|
||||
errorMessage ? errorMessage : 'Error performing action'
|
||||
errorMessage ? errorMessage : 'Error performing action',
|
||||
jsonBody
|
||||
);
|
||||
if (onclick) {
|
||||
await onclick();
|
||||
@@ -71,7 +74,7 @@
|
||||
|
||||
<button
|
||||
class="text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row items-center gap-1 {buttonClasses}"
|
||||
onclick={handleClick}
|
||||
onclick={handle_click}
|
||||
disabled={is_disabled}
|
||||
aria-label={ariaLabel || label}
|
||||
>
|
||||
|
||||
121
daemon/web/src/lib/components/ClockDriftAlert.svelte
Normal file
121
daemon/web/src/lib/components/ClockDriftAlert.svelte
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import { get_daemon_time } from '$lib/utils.svelte';
|
||||
import ApiRequestButton from './ApiRequestButton.svelte';
|
||||
|
||||
let show_alert = $state(false);
|
||||
let device_system_time = $state('');
|
||||
let device_adjusted_time = $state('');
|
||||
let browser_time = $state('');
|
||||
let has_offset = $state(false);
|
||||
let computed_offset = $state(0);
|
||||
let dismissed = $state(false);
|
||||
let check_completed = $state(false);
|
||||
|
||||
const DRIFT_THRESHOLD_SECONDS = 30;
|
||||
|
||||
function format_time(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
async function check_clock_drift() {
|
||||
if (check_completed) return;
|
||||
|
||||
try {
|
||||
const daemon_time_response = await get_daemon_time();
|
||||
const browser_now = new Date();
|
||||
const daemon_system_ms = new Date(daemon_time_response.system_time).getTime();
|
||||
const device_adjusted_ms = new Date(daemon_time_response.adjusted_time).getTime();
|
||||
const drift_seconds = Math.round((browser_now.getTime() - device_adjusted_ms) / 1000);
|
||||
|
||||
if (Math.abs(drift_seconds) > DRIFT_THRESHOLD_SECONDS && !dismissed) {
|
||||
device_system_time = format_time(new Date(daemon_time_response.system_time));
|
||||
device_adjusted_time = format_time(new Date(daemon_time_response.adjusted_time));
|
||||
browser_time = format_time(browser_now);
|
||||
has_offset = daemon_time_response.offset_seconds !== 0;
|
||||
// Calculate offset needed: browser_time - daemon_system_time
|
||||
computed_offset = Math.round((browser_now.getTime() - daemon_system_ms) / 1000);
|
||||
show_alert = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check clock drift:', err);
|
||||
}
|
||||
check_completed = true;
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
show_alert = false;
|
||||
dismissed = true;
|
||||
}
|
||||
|
||||
// Check clock drift on component mount
|
||||
$effect(() => {
|
||||
check_clock_drift();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if show_alert}
|
||||
<div
|
||||
class="bg-yellow-100 border-yellow-400 drop-shadow p-4 flex flex-col gap-2 border rounded-md"
|
||||
>
|
||||
<span class="text-xl font-bold flex flex-row items-center gap-2 text-yellow-700">
|
||||
<svg
|
||||
class="w-6 h-6 text-yellow-600"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Clock Mismatch Detected
|
||||
</span>
|
||||
<p>
|
||||
Rayhunter's clock doesn't match your browser's, and may be incorrect. This can happen if
|
||||
Rayhunter is unable to get the correct time from the internet. Consider synchronizing
|
||||
your browser's clock with the button below, or using another SIM card for better
|
||||
results.
|
||||
</p>
|
||||
<table class="w-fit">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pr-2">Rayhunter clock (system):</td>
|
||||
<td class="font-mono">{device_system_time}</td>
|
||||
</tr>
|
||||
{#if has_offset}
|
||||
<tr>
|
||||
<td class="pr-2">Rayhunter clock (adjusted):</td>
|
||||
<td class="font-mono">{device_adjusted_time}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<td class="pr-2">Browser clock:</td>
|
||||
<td class="font-mono">{browser_time}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Copy browser clock to device?</p>
|
||||
<div class="flex flex-row gap-2 justify-end">
|
||||
<button
|
||||
class="font-medium py-2 px-4 rounded-md border border-gray-400 hover:bg-yellow-200"
|
||||
onclick={dismiss}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
<ApiRequestButton
|
||||
url="/api/time-offset"
|
||||
label="Sync Clock"
|
||||
loadingLabel="Syncing..."
|
||||
variant="green"
|
||||
jsonBody={{ offset_seconds: computed_offset }}
|
||||
onclick={dismiss}
|
||||
errorMessage="Error syncing clock"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { get_config, set_config, type Config } from '../utils.svelte';
|
||||
import { get_config, set_config, test_notification, type Config } from '../utils.svelte';
|
||||
|
||||
let config = $state<Config | null>(null);
|
||||
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
let testingNotification = $state(false);
|
||||
let message = $state('');
|
||||
let messageType = $state<'success' | 'error' | null>(null);
|
||||
let testMessage = $state('');
|
||||
let testMessageType = $state<'success' | 'error' | null>(null);
|
||||
let showConfig = $state(false);
|
||||
|
||||
async function loadConfig() {
|
||||
async function load_config() {
|
||||
try {
|
||||
loading = true;
|
||||
config = await get_config();
|
||||
@@ -23,7 +26,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
async function save_config() {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
@@ -40,10 +43,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Load config when first shown
|
||||
async function send_test_notification() {
|
||||
try {
|
||||
testingNotification = true;
|
||||
testMessage = '';
|
||||
testMessageType = null;
|
||||
await test_notification();
|
||||
testMessage = 'Test notification sent successfully!';
|
||||
testMessageType = 'success';
|
||||
} catch (error) {
|
||||
testMessage = `${error}`;
|
||||
testMessageType = 'error';
|
||||
} finally {
|
||||
testingNotification = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showConfig && !config) {
|
||||
loadConfig();
|
||||
load_config();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -73,7 +91,7 @@
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveConfig();
|
||||
save_config();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
@@ -89,7 +107,12 @@
|
||||
<option value={1}>1 - Subtle mode (colored line)</option>
|
||||
<option value={2}>2 - Demo mode (orca gif)</option>
|
||||
<option value={3}>3 - EFF logo</option>
|
||||
<option value={4}>4 - High visibility (full screen color)</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Note: Rayhunter draws over the device's native UI, so some flickering is
|
||||
expected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -138,6 +161,49 @@
|
||||
bind:value={config.ntfy_url}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Test button below uses the saved configuration URL, not the input above
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={send_test_notification}
|
||||
disabled={testingNotification}
|
||||
class="bg-rayhunter-blue hover:bg-rayhunter-dark-blue disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
||||
>
|
||||
{#if testingNotification}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
Sending...
|
||||
{:else}
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
></path>
|
||||
</svg>
|
||||
Send Test Notification
|
||||
{/if}
|
||||
</button>
|
||||
{#if testMessage}
|
||||
<div
|
||||
class="mt-2 p-2 rounded text-sm {testMessageType === 'error'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-green-100 text-green-700'}"
|
||||
>
|
||||
{testMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
name: string;
|
||||
} = $props();
|
||||
|
||||
function confirmDelete() {
|
||||
function confirm_delete() {
|
||||
if (window.confirm(prompt)) {
|
||||
user_action_req('POST', url, 'Unable to delete recording ' + name);
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<button
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
|
||||
onclick={confirmDelete}
|
||||
onclick={confirm_delete}
|
||||
aria-label="delete"
|
||||
>
|
||||
<p>{text}</p>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
analysis_status === AnalysisStatus.Queued || analysis_status === AnalysisStatus.Running
|
||||
);
|
||||
|
||||
async function handleReAnalyze() {
|
||||
async function handle_re_analyze() {
|
||||
// Update the entry directly for immediate UI feedback
|
||||
entry.analysis_status = AnalysisStatus.Queued;
|
||||
entry.analysis_report = undefined;
|
||||
@@ -33,7 +33,7 @@
|
||||
loadingLabel="Analyzing..."
|
||||
disabled={is_processing}
|
||||
variant="blue"
|
||||
onclick={handleReAnalyze}
|
||||
onclick={handle_re_analyze}
|
||||
ariaLabel="re-analyze"
|
||||
errorMessage="Error re-analyzing recoding"
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { breakpoints } from '../../theme';
|
||||
type Breakpoint = keyof typeof breakpoints;
|
||||
|
||||
// Store that tracks if a specific breakpoint matches
|
||||
export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean> {
|
||||
export function create_breakpoint_store(breakpoint: Breakpoint): Readable<boolean> {
|
||||
return readable<boolean>(false, (set) => {
|
||||
const width = breakpoints[breakpoint];
|
||||
const mediaQuery = window.matchMedia(`(min-width: ${width})`);
|
||||
@@ -23,7 +23,7 @@ export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean>
|
||||
}
|
||||
|
||||
// Create stores for each breakpoint
|
||||
export const screenIsSmUp: Readable<boolean> = createBreakpointStore('sm');
|
||||
export const screenIsMdUp: Readable<boolean> = createBreakpointStore('md');
|
||||
export const screenIsLgUp: Readable<boolean> = createBreakpointStore('lg');
|
||||
export const screenIsXlUp: Readable<boolean> = createBreakpointStore('xl');
|
||||
export const screenIsSmUp: Readable<boolean> = create_breakpoint_store('sm');
|
||||
export const screenIsMdUp: Readable<boolean> = create_breakpoint_store('md');
|
||||
export const screenIsLgUp: Readable<boolean> = create_breakpoint_store('lg');
|
||||
export const screenIsXlUp: Readable<boolean> = create_breakpoint_store('xl');
|
||||
|
||||
@@ -26,15 +26,18 @@ export interface Config {
|
||||
analyzers: AnalyzerConfig;
|
||||
}
|
||||
|
||||
export async function req(method: string, url: string): Promise<string> {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
});
|
||||
const body = await response.text();
|
||||
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
|
||||
const options: RequestInit = { method };
|
||||
if (json_body !== undefined) {
|
||||
options.body = JSON.stringify(json_body);
|
||||
options.headers = { 'Content-Type': 'application/json' };
|
||||
}
|
||||
const response = await fetch(url, options);
|
||||
const responseBody = await response.text();
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return body;
|
||||
return responseBody;
|
||||
} else {
|
||||
throw new Error(body);
|
||||
throw new Error(responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +45,13 @@ export async function req(method: string, url: string): Promise<string> {
|
||||
export async function user_action_req(
|
||||
method: string,
|
||||
url: string,
|
||||
error_msg: string
|
||||
error_msg: string,
|
||||
json_body?: unknown
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
return await req(method, url);
|
||||
return await req(method, url, json_body);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('beeeo');
|
||||
add_error(error, error_msg);
|
||||
}
|
||||
return undefined;
|
||||
@@ -86,3 +89,24 @@ export async function set_config(config: Config): Promise<void> {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function test_notification(): Promise<void> {
|
||||
const response = await fetch('/api/test-notification', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TimeResponse {
|
||||
system_time: string;
|
||||
adjusted_time: string;
|
||||
offset_seconds: number;
|
||||
}
|
||||
|
||||
export async function get_daemon_time(): Promise<TimeResponse> {
|
||||
return JSON.parse(await req('GET', '/api/time'));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import RecordingControls from '$lib/components/RecordingControls.svelte';
|
||||
import ConfigForm from '$lib/components/ConfigForm.svelte';
|
||||
import ActionErrors from '$lib/components/ActionErrors.svelte';
|
||||
import ClockDriftAlert from '$lib/components/ClockDriftAlert.svelte';
|
||||
import LogView from '$lib/components/LogView.svelte';
|
||||
|
||||
let manager: AnalysisManager = new AnalysisManager();
|
||||
@@ -102,6 +103,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px bg-white/30 self-stretch"></div>
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
href="https://github.com/EFForg/rayhunter/issues"
|
||||
@@ -148,6 +150,26 @@
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
href="https://supporters.eff.org/donate"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Donate</span>
|
||||
<svg
|
||||
class="w-6 h-6 text-white group-hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="m12.75 20.66 6.184-7.098c2.677-2.884 2.559-6.506.754-8.705-.898-1.095-2.206-1.816-3.72-1.855-1.293-.034-2.652.43-3.963 1.537-1.31-1.108-2.67-1.571-3.962-1.537-1.515.04-2.823.76-3.72 1.855-1.806 2.2-1.924 5.821.753 8.705l6.184 7.098.245.281a.75.75 0 0 0 1.09 0l.246-.281Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-4 xl:mx-8 flex flex-col gap-4">
|
||||
@@ -186,6 +208,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<ActionErrors />
|
||||
<ClockDriftAlert />
|
||||
{#if loaded}
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
{#if current_entry}
|
||||
|
||||
1
dist/config.toml.in
vendored
1
dist/config.toml.in
vendored
@@ -12,6 +12,7 @@ colorblind_mode = false
|
||||
# 1 = Subtle mode, display a colored line at the top of the screen when rayhunter is running (green=running, white=paused, red=warnings)
|
||||
# 2 = Demo Mode, display a fun orca gif
|
||||
# 3 = display the EFF logo
|
||||
# 4 = High Visibility mode, fill the entire screen with the status color (green=running, white=paused, red=warnings)
|
||||
#
|
||||
# TP-Link with one-bit display:
|
||||
# 0 = invisible mode
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](./introduction.md)
|
||||
- [Support, feedback, and community](./support-feedback-community.md)
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
- [Installation](./installation.md)
|
||||
- [Installing from the latest release](./installing-from-release.md)
|
||||
- [Installing from source](./installing-from-source.md)
|
||||
@@ -20,5 +22,3 @@
|
||||
- [Wingtech CT2MHS01](./wingtech-ct2mhs01.md)
|
||||
- [PinePhone and PinePhone Pro](./pinephone.md)
|
||||
- [Moxee Hotspot](./moxee.md)
|
||||
- [Support, feedback, and community](./support-feedback-community.md)
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
|
||||
@@ -9,7 +9,8 @@ Through web UI you can set:
|
||||
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
|
||||
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording.
|
||||
- *Demo mode (orca gif)*, which shows image of orcas *and* colored line.
|
||||
- *EFF logo*, which shows EFF logo and *and* colored line.
|
||||
- *EFF logo*, which shows EFF logo *and* colored line.
|
||||
- *High visibility (full screen color)*: fills the entire screen with the status color (green for recording, red for warnings, white for paused).
|
||||
- **Device Input Mode**, which defines behavior of built-in power button of the device. *Device Input Mode* could be:
|
||||
- *Disable button control*: built-in power button of the device is not used by Rayhunter.
|
||||
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
||||
|
||||
29
doc/faq.md
29
doc/faq.md
@@ -24,18 +24,27 @@ If you want to use a non-Verizon SIM card you will probably need an unlocked dev
|
||||
|
||||
### How do I re-enable USB tethering after installing Rayhunter?
|
||||
|
||||
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
||||
If you have installed with `./installer orbic-usb`, you might find that USB
|
||||
tethering is now disabled. If you have run `./installer orbic`, this section is not
|
||||
relevant as it does not use or touch USB.
|
||||
|
||||
[First obtain a shell](./orbic.md#shell), then:
|
||||
|
||||
|
||||
```sh
|
||||
./installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
# inside of Orbic's shell:
|
||||
echo 9 > /usrdata/mode.cfg
|
||||
reboot
|
||||
```
|
||||
|
||||
Make sure USB tethering is also enabled in the Orbic's UI.
|
||||
|
||||
To disable tethering again:
|
||||
|
||||
```sh
|
||||
./installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
# inside of Orbic's shell:
|
||||
echo 3 > /usrdata/mode.cfg
|
||||
reboot
|
||||
```
|
||||
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
@@ -43,16 +52,16 @@ See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB g
|
||||
|
||||
### How do I disable the WiFi hotspot on the Orbic RC400L?
|
||||
|
||||
To disable both WiFi bands:
|
||||
To disable both WiFi bands, [first obtain a shell](./orbic.md#shell), then:
|
||||
|
||||
```sh
|
||||
adb shell
|
||||
/bin/rootshell -c "sed -i 's/<wlan><Feature><state>1<\/state>/<wlan><Feature><state>0<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
|
||||
# inside of Orbic's shell:
|
||||
sed -i 's/<wlan><Feature><state>1<\/state>/<wlan><Feature><state>0<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot
|
||||
```
|
||||
|
||||
To re-enable WiFi:
|
||||
|
||||
```sh
|
||||
adb shell
|
||||
/bin/rootshell -c "sed -i 's/<wlan><Feature><state>0<\/state>/<wlan><Feature><state>1<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
|
||||
# inside of Orbic's shell:
|
||||
sed -i 's/<wlan><Feature><state>0<\/state>/<wlan><Feature><state>1<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@ This heuristic will also issue a notification every time your identity is sent t
|
||||
This analyzer tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||
|
||||
|
||||
### LTE SIB6/7 Downgrade
|
||||
### LTE SIB6/7 Downgrade (v2)
|
||||
|
||||
This analyzer tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
||||
|
||||
@@ -49,7 +49,7 @@ This attack exploits the fact that SIB broadcast messages are not encrypted or a
|
||||
|
||||
SIB6 is used for cell reselection to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
||||
|
||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristic may trigger false positives. In that case you should consider disabling it. However this heuristic has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||
This heuristic is useful even in countries where 2g is still prevalent. A well behaved tower should always advertise its other 4g neighbors at a higher priority than 2g/3g neighbors. (Older versions of this heuristic were prone to false positives.)
|
||||
|
||||
### Null Cipher
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
```bash
|
||||
# For Orbic:
|
||||
./installer orbic --admin-password 'mypassword'
|
||||
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
|
||||
# may be required if different from the default.
|
||||
|
||||
# Or install over USB if you want ADB and a root shell (not recommended for most users)
|
||||
./installer orbic-usb
|
||||
|
||||
@@ -48,7 +51,8 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
./installer tplink
|
||||
```
|
||||
|
||||
* On Verizon Orbic, the password is the WiFi password.
|
||||
* On Verizon Orbic, the password is the one used to login to the device's admin menu, and the default is the WiFi password.
|
||||
* ***Note:*** If you have changed the device username, password, or IP address from their default values, these must be provided as arguments to the installer command above.
|
||||
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
|
||||
* On Moxee-brand devices, check under the battery for the password.
|
||||
* You can reset the password by pressing the button under the back case until the unit restarts.
|
||||
|
||||
@@ -47,12 +47,11 @@ cargo build-daemon-firmware-devel
|
||||
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
|
||||
|
||||
# Build rootshell
|
||||
cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
|
||||
cargo build-rootshell-firmware-devel
|
||||
|
||||
# Replace 'orbic' with your device type if different.
|
||||
# A list of possible values can be found with 'cargo run --bin installer help'.
|
||||
# Use FILE_RAYHUNTER_DAEMON to specify the daemon binary path when using development builds:
|
||||
FILE_RAYHUNTER_DAEMON=$PWD/target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon cargo run -p installer --bin installer orbic
|
||||
FIRMWARE_PROFILE=firmware-devel cargo run -p installer --bin installer orbic
|
||||
```
|
||||
|
||||
### If you're on Windows or can't run the install scripts
|
||||
|
||||
@@ -6,7 +6,8 @@ It is also sometimes sold under the brand Kajeet RC400L. This is the exact same
|
||||
|
||||
You can buy an Orbic [using bezos
|
||||
bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y),
|
||||
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
||||
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l). You should not
|
||||
pay more than 30 USD for such a device (without shipping).
|
||||
|
||||
[Please check whether the Orbic works in your country](https://www.frequencycheck.com/countries/), and whether the Orbic RC400L supports the right frequency bands for your purpose before buying.
|
||||
|
||||
@@ -37,6 +38,7 @@ The orbic's installation routine underwent many different changes:
|
||||
It's possible that many tutorials out there still refer to some of the old
|
||||
installation routines.
|
||||
|
||||
<a name=shell></a>
|
||||
## Obtaining a shell
|
||||
|
||||
After running the installer, there will not be a rootshell and ADB will not be
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
If you're using Rayhunter (or trying to), we'd love to hear from you! Check out one of the following forums for contacting the Rayhunter developers and community:
|
||||
|
||||
* If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (QMDL and PCAP logs) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
||||
* If you're having issues installing or using Rayhunter, please [open an issue](https://github.com/EFForg/rayhunter/issues) on our Github repo.
|
||||
* If you've received a Rayhunter warning, please send your Rayhunter data captures (the ZIP file) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version.
|
||||
|
||||
Note that the recording files are sensitive data and contain location
|
||||
information, so we strongly recommend against posting them to publicly.
|
||||
|
||||
If you're unfamiliar with Signal, feel free to check out our [Security Self
|
||||
Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
||||
|
||||
* If you're having issues installing or using Rayhunter, consider checking the [Frequently Asked Questions](./faq.md) page for answers to common questions.
|
||||
* If your question isn't answered there, please [open an issue](https://github.com/EFForg/rayhunter/issues) on our Github repo.
|
||||
* If you'd like to propose a feature, heuristic, or device for Rayhunter, [start a discussion](https://github.com/EFForg/rayhunter/discussions) in our Github repo
|
||||
* For anything else, join us in the `#rayhunter` or `#rayhunter-developers` channel of [EFF's Mattermost](https://opensource.eff.org/signup_user_complete/?id=r1b6cnta9bysxk6im3kuabiu1y&md=link&sbr=su) instance to chat!
|
||||
|
||||
@@ -35,6 +35,7 @@ You can get your TP-Link M7350 from:
|
||||
* First check for used offers on local sites, sometimes it's much cheaper there.
|
||||
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350).
|
||||
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313).
|
||||
* Can also be found sold as the 'Vodafone Pocket Wifi 5' in Australia
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@ You'll need to install [Tauri's dependencies](https://tauri.app/start/prerequisi
|
||||
|
||||
The GUI installer pulls in the CLI installer as a library. Like with the CLI installer, the firmware binary needs to be present and can be overridden with the same envvars. See `../installer/build.rs` for options.
|
||||
|
||||
For example, to build the firmware in development mode and then provide the path explicitly:
|
||||
For example, to build the firmware in development mode:
|
||||
|
||||
```bash
|
||||
cargo build-daemon-firmware-devel
|
||||
cargo build-rootshell-firmware-devel
|
||||
|
||||
(cd installer-gui && FILE_RAYHUNTER_DAEMON=$PWD/../target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon npm run tauri android build)
|
||||
(cd installer-gui && FIRMWARE_PROFILE=firmware-devel npm run tauri android build)
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
20
installer-gui/package-lock.json
generated
20
installer-gui/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"eslint": "^9.38.0",
|
||||
@@ -1087,9 +1087,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.48.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.3.tgz",
|
||||
"integrity": "sha512-jf8mx3yctRXE9hvixgcqqK94YI2hDnbxI/12Upkz99XFMvxnJKCMzvz0j7lmbXSyBSNEycWO5xHvi7b73y9qkQ==",
|
||||
"version": "2.50.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.1.tgz",
|
||||
"integrity": "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.3.2",
|
||||
"devalue": "^5.6.2",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -1117,11 +1117,15 @@
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2267,9 +2271,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz",
|
||||
"integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"prepare": "svelte-kit sync",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -23,7 +24,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"eslint": "^9.38.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer-gui"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
@@ -17,7 +17,7 @@ anyhow = "1.0.98"
|
||||
axum = { version = "0.8.3", features = ["http1", "tokio"], default-features = false }
|
||||
base64_light = "0.1.5"
|
||||
block-padding = "0.3.3"
|
||||
bytes = "1.10.1"
|
||||
bytes = "1.11.1"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
hyper = "1.6.0"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use core::str;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||
let include_dir = Path::new(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../target/armv7-unknown-linux-musleabihf/firmware/"
|
||||
));
|
||||
set_binary_var(include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(include_dir, "FILE_RAYHUNTER_DAEMON", "rayhunter-daemon");
|
||||
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| "firmware".to_string());
|
||||
let include_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../target/armv7-unknown-linux-musleabihf")
|
||||
.join(&profile);
|
||||
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(&include_dir, "FILE_RAYHUNTER_DAEMON", "rayhunter-daemon");
|
||||
}
|
||||
|
||||
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
println!("cargo::rerun-if-env-changed={var}");
|
||||
if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
@@ -23,6 +24,7 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
}
|
||||
if std::env::var_os(var).is_none() {
|
||||
let binary = include_dir.join(file);
|
||||
println!("cargo::rerun-if-changed={}", binary.display());
|
||||
if !binary.exists() {
|
||||
println!(
|
||||
"cargo::error=Firmware binary {file} not present at {}",
|
||||
@@ -31,6 +33,5 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
exit(0);
|
||||
}
|
||||
println!("cargo::rustc-env={var}={}", binary.display());
|
||||
println!("cargo::rerun-if-changed={}", binary.display());
|
||||
}
|
||||
}
|
||||
|
||||
68
installer/src/connection.rs
Normal file
68
installer/src/connection.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::output::println;
|
||||
|
||||
/// Abstraction for device communication (telnet or ADB)
|
||||
pub trait DeviceConnection {
|
||||
/// Run a shell command and return its output
|
||||
fn run_command(&mut self, command: &str) -> impl Future<Output = Result<String>> + Send;
|
||||
|
||||
/// Write a file to the device
|
||||
fn write_file(&mut self, path: &str, content: &[u8])
|
||||
-> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
/// Check if a file exists using a DeviceConnection
|
||||
pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
|
||||
conn.run_command(&format!("test -f {path} && echo exists || echo missing"))
|
||||
.await
|
||||
.map(|output| output.contains("exists"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Shared config installation logic
|
||||
pub async fn install_config<C: DeviceConnection>(
|
||||
conn: &mut C,
|
||||
config_path: &str,
|
||||
device_type: &str,
|
||||
reset_config: bool,
|
||||
) -> Result<()> {
|
||||
if reset_config || !file_exists(conn, config_path).await {
|
||||
let config = crate::CONFIG_TOML.replace(
|
||||
r#"#device = "orbic""#,
|
||||
&format!(r#"device = "{device_type}""#),
|
||||
);
|
||||
conn.write_file(config_path, config.as_bytes()).await?;
|
||||
} else {
|
||||
println!("Config file already exists, skipping (use --reset-config to overwrite)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet-based connection wrapper
|
||||
pub struct TelnetConnection {
|
||||
pub addr: SocketAddr,
|
||||
pub wait_for_prompt: bool,
|
||||
}
|
||||
|
||||
impl TelnetConnection {
|
||||
pub fn new(addr: SocketAddr, wait_for_prompt: bool) -> Self {
|
||||
Self {
|
||||
addr,
|
||||
wait_for_prompt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceConnection for TelnetConnection {
|
||||
async fn run_command(&mut self, command: &str) -> Result<String> {
|
||||
crate::util::telnet_send_command_with_output(self.addr, command, self.wait_for_prompt).await
|
||||
}
|
||||
|
||||
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
|
||||
crate::util::telnet_send_file(self.addr, path, content, self.wait_for_prompt).await
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use env_logger::Env;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use anyhow::bail;
|
||||
|
||||
mod connection;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod orbic;
|
||||
mod orbic_auth;
|
||||
@@ -79,10 +80,18 @@ struct InstallTpLink {
|
||||
/// your custom path may conflict with the builtin storage functionality.
|
||||
#[arg(long, default_value = "")]
|
||||
sdcard_path: String,
|
||||
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallOrbic {}
|
||||
struct InstallOrbic {
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct OrbicNetworkArgs {
|
||||
@@ -97,6 +106,10 @@ struct OrbicNetworkArgs {
|
||||
/// Admin password for authentication.
|
||||
#[arg(long)]
|
||||
admin_password: Option<String>,
|
||||
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -231,8 +244,8 @@ async fn run(args: Args) -> Result<(), Error> {
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Command::OrbicUsb(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
|
||||
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||
Command::OrbicUsb(args) => orbic::install(args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
|
||||
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => {
|
||||
match subcommand.command {
|
||||
|
||||
@@ -12,9 +12,10 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::RAYHUNTER_DAEMON_INIT;
|
||||
use crate::connection::{DeviceConnection, install_config};
|
||||
use crate::output::{print, println};
|
||||
use crate::util::open_usb_device;
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||
Make sure your device is plugged in and turned on.
|
||||
@@ -46,6 +47,21 @@ const PRODUCT_ID: u16 = 0xf601;
|
||||
|
||||
const INTERFACE: u8 = 1;
|
||||
|
||||
/// ADB-based connection wrapper for DeviceConnection trait
|
||||
pub struct AdbConnection<'a> {
|
||||
device: &'a mut ADBUSBDevice,
|
||||
}
|
||||
|
||||
impl DeviceConnection for AdbConnection<'_> {
|
||||
async fn run_command(&mut self, command: &str) -> Result<String> {
|
||||
adb_command(self.device, &["sh", "-c", command])
|
||||
}
|
||||
|
||||
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
|
||||
install_file(self.device, path, content).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const RNDIS_INTERFACE: u8 = 0;
|
||||
|
||||
@@ -61,7 +77,7 @@ async fn confirm() -> Result<bool> {
|
||||
Ok(input.trim() == "yes")
|
||||
}
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
pub async fn install(reset_config: bool) -> Result<()> {
|
||||
println!(
|
||||
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes."
|
||||
);
|
||||
@@ -80,7 +96,7 @@ pub async fn install() -> Result<()> {
|
||||
setup_rootshell(&mut adb_device).await?;
|
||||
println!("done");
|
||||
print!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||
let mut adb_device = setup_rayhunter(adb_device, reset_config).await?;
|
||||
println!("done");
|
||||
print!("Testing rayhunter... ");
|
||||
test_rayhunter(&mut adb_device).await?;
|
||||
@@ -127,7 +143,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
|
||||
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
|
||||
@@ -137,14 +153,20 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
&mut adb_device,
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"orbic\"")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut conn = AdbConnection {
|
||||
device: &mut adb_device,
|
||||
};
|
||||
install_config(
|
||||
&mut conn,
|
||||
"/data/rayhunter/config.toml",
|
||||
"orbic",
|
||||
reset_config,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
install_file(
|
||||
&mut adb_device,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
|
||||
@@ -7,10 +7,11 @@ use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::RAYHUNTER_DAEMON_INIT;
|
||||
use crate::connection::{TelnetConnection, install_config};
|
||||
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
||||
use crate::output::{eprintln, print, println};
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
|
||||
const TELNET_PORT: u16 = 24;
|
||||
@@ -142,6 +143,7 @@ pub async fn install(
|
||||
admin_ip: String,
|
||||
admin_username: String,
|
||||
admin_password: Option<String>,
|
||||
reset_config: bool,
|
||||
) -> Result<()> {
|
||||
let Some(admin_password) = admin_password else {
|
||||
eprintln!(
|
||||
@@ -165,7 +167,7 @@ pub async fn install(
|
||||
wait_for_telnet(&admin_ip).await?;
|
||||
println!("done");
|
||||
|
||||
setup_rayhunter(&admin_ip).await
|
||||
setup_rayhunter(&admin_ip, reset_config).await
|
||||
}
|
||||
|
||||
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
@@ -189,7 +191,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
@@ -213,13 +215,12 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
let mut conn = TelnetConnection::new(addr, false);
|
||||
install_config(
|
||||
&mut conn,
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace(r#"#device = "orbic""#, r#"device = "orbic""#)
|
||||
.as_bytes(),
|
||||
false,
|
||||
"orbic",
|
||||
reset_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use nusb::Interface;
|
||||
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::connection::DeviceConnection;
|
||||
use crate::orbic::test_rayhunter;
|
||||
use crate::output::{print, println};
|
||||
use crate::util::open_usb_device;
|
||||
@@ -25,33 +26,39 @@ pub async fn install() -> Result<()> {
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
|
||||
adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?;
|
||||
run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?;
|
||||
run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?;
|
||||
adb.install_file(
|
||||
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"pinephone\"")
|
||||
.as_bytes(),
|
||||
)?;
|
||||
adb.install_file(
|
||||
)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)?;
|
||||
adb.install_file(
|
||||
)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
"/etc/init.d/misc-daemon",
|
||||
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||
)?;
|
||||
adb.run_command(
|
||||
&["chmod", "755", "/etc/init.d/rayhunter_daemon"],
|
||||
)
|
||||
.await?;
|
||||
run_command_expect(
|
||||
&mut adb,
|
||||
"chmod 755 /etc/init.d/rayhunter_daemon",
|
||||
"exit code 0",
|
||||
)?;
|
||||
adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?;
|
||||
)
|
||||
.await?;
|
||||
run_command_expect(&mut adb, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
|
||||
run_command_expect(&mut adb, "shutdown -r -t 1 now", "exit code 0").await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
print!("Unlocking modem ... ");
|
||||
@@ -68,6 +75,19 @@ pub async fn install() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to run a command and check for expected output
|
||||
async fn run_command_expect(
|
||||
adb: &mut ADBUSBDevice,
|
||||
command: &str,
|
||||
expected_output: &str,
|
||||
) -> Result<()> {
|
||||
let output = adb.run_command(command).await?;
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Qusbcfg {
|
||||
vendor_id: u16,
|
||||
product_id: u16,
|
||||
@@ -175,29 +195,18 @@ pub async fn stop_adb() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
trait Install {
|
||||
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>;
|
||||
fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Install for ADBUSBDevice {
|
||||
/// Run an adb shell command, append '; echo exit code $?' to the command and verify its output.
|
||||
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> {
|
||||
impl DeviceConnection for ADBUSBDevice {
|
||||
/// Run an adb shell command, append '; echo exit code $?' to the command and return output.
|
||||
async fn run_command(&mut self, command: &str) -> Result<String> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
let mut cmd = Vec::<&str>::new();
|
||||
cmd.extend_from_slice(command);
|
||||
cmd.extend_from_slice(&[";", "echo", "exit code $?"]);
|
||||
let cmd = ["sh", "-c", &format!("{command}; echo exit code $?")];
|
||||
self.shell_command(&cmd, &mut buf)?;
|
||||
let output = String::from_utf8_lossy(&buf);
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
Ok(())
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
|
||||
/// Transfer a file to the modem's filesystem with adb push.
|
||||
/// Validates the file sends successfully to /tmp before overwriting the destination.
|
||||
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
||||
async fn write_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
||||
print!("Sending file {dest} ... ");
|
||||
let file_name = Path::new(dest)
|
||||
.file_name()
|
||||
@@ -208,8 +217,16 @@ impl Install for ADBUSBDevice {
|
||||
let push_tmp_path = format!("/tmp/{file_name}");
|
||||
let file_hash = md5_compute(payload);
|
||||
self.push(&mut payload, &push_tmp_path)?;
|
||||
self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?;
|
||||
self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?;
|
||||
let output = self.run_command(&format!("md5sum {push_tmp_path}")).await?;
|
||||
if !output.contains(&format!("{file_hash:x}")) {
|
||||
bail!("{:x} not found in: {output}", file_hash);
|
||||
}
|
||||
let output = self
|
||||
.run_command(&format!("mv {push_tmp_path} {dest}"))
|
||||
.await?;
|
||||
if !output.contains("exit code 0") {
|
||||
bail!("exit code 0 not found in: {output}");
|
||||
}
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::InstallTpLink;
|
||||
use crate::connection::{TelnetConnection, install_config};
|
||||
use crate::output::println;
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
|
||||
@@ -28,10 +29,11 @@ pub async fn main_tplink(
|
||||
skip_sdcard,
|
||||
admin_ip,
|
||||
sdcard_path,
|
||||
reset_config,
|
||||
}: InstallTpLink,
|
||||
) -> Result<(), Error> {
|
||||
let is_v3 = start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
|
||||
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -111,6 +113,7 @@ async fn tplink_run_install(
|
||||
admin_ip: String,
|
||||
mut sdcard_path: String,
|
||||
is_v3: bool,
|
||||
reset_config: bool,
|
||||
) -> Result<(), Error> {
|
||||
println!("Connecting via telnet to {admin_ip}");
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
@@ -181,15 +184,9 @@ async fn tplink_run_install(
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
&format!("{sdcard_path}/config.toml"),
|
||||
crate::CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"tplink\"")
|
||||
.as_bytes(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let mut conn = TelnetConnection::new(addr, true);
|
||||
let config_path = format!("{sdcard_path}/config.toml");
|
||||
install_config(&mut conn, &config_path, "tplink", reset_config).await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
@@ -10,12 +10,13 @@ name = "rayhunter"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5.0"
|
||||
bytes = "1.11.1"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
crc = "3.0.1"
|
||||
deku = { version = "0.20.0", features = ["logging"] }
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
nix = { version = "0.29.0", features = ["feature"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
pycrate-rs = { git = "https://github.com/EFForg/pycrate-rs" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use log::debug;
|
||||
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
@@ -327,7 +328,7 @@ impl Harness {
|
||||
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
|
||||
}
|
||||
if analyzer_config.lte_sib6_and_7_downgrade {
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer::new()));
|
||||
}
|
||||
if analyzer_config.null_cipher {
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer {}));
|
||||
@@ -380,8 +381,12 @@ impl Harness {
|
||||
row.events = match InformationElement::try_from(&gsmtap_message) {
|
||||
Ok(element) => self.analyze_information_element(&element),
|
||||
Err(err) => {
|
||||
row.skipped_message_reason =
|
||||
Some(format!("failed to convert gsmtap message to IE: {err:?}"));
|
||||
let msg = format!(
|
||||
"in packet {}, failed to convert gsmtap message to IE: {err:?}",
|
||||
self.packet_num
|
||||
);
|
||||
debug!("{msg}");
|
||||
row.skipped_message_reason = Some(msg);
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::borrow::Cow;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
use log::debug;
|
||||
use telcom_parser::lte_rrc::{
|
||||
BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1, CellReselectionPriority,
|
||||
SystemInformation_r8_IEsSib_TypeAndInfo, SystemInformation_r8_IEsSib_TypeAndInfo_Entry,
|
||||
@@ -9,9 +10,24 @@ use telcom_parser::lte_rrc::{
|
||||
};
|
||||
|
||||
/// Based on heuristic T7 from Shinjo Park's "Why We Cannot Win".
|
||||
pub struct LteSib6And7DowngradeAnalyzer {}
|
||||
pub struct LteSib6And7DowngradeAnalyzer {
|
||||
lte_priority: Option<u8>,
|
||||
legacy_priority: Option<u8>,
|
||||
}
|
||||
impl Default for LteSib6And7DowngradeAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LteSib6And7DowngradeAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
lte_priority: None,
|
||||
legacy_priority: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_system_information<'a>(
|
||||
&self,
|
||||
ie: &'a InformationElement,
|
||||
@@ -43,7 +59,7 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
}
|
||||
|
||||
fn get_version(&self) -> u32 {
|
||||
1
|
||||
2
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
@@ -51,22 +67,78 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<super::analyzer::Event> {
|
||||
if let InformationElement::LTE(lte_ie) = ie
|
||||
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
|
||||
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
|
||||
&& let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(_) = c1
|
||||
{
|
||||
debug!("encountered sib1 at {_packet_num}");
|
||||
let flag = if self.legacy_priority > self.lte_priority {
|
||||
if self.lte_priority.is_none() {
|
||||
Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message:
|
||||
"LTE cell advertised a legacy (3G/2G) neighbors but no LTE neighbors"
|
||||
.to_string(),
|
||||
})
|
||||
} else {
|
||||
Some(Event {
|
||||
event_type: EventType::High,
|
||||
message:
|
||||
format!("LTE cell advertised a legacy (3G/2G) cell for priority {:?} reselection over LTE neighbors at priority {:?}", self.legacy_priority?, self.lte_priority)
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
debug!(
|
||||
"flag is {flag:?} because lte priority is {:?} and legacy priority is {:?} ",
|
||||
self.lte_priority, self.legacy_priority
|
||||
);
|
||||
self.lte_priority = None;
|
||||
self.legacy_priority = None;
|
||||
debug!("reset priority to 0 due to new sib1 at {_packet_num}");
|
||||
return flag;
|
||||
}
|
||||
|
||||
let sibs = &self.unpack_system_information(ie)?.0;
|
||||
for sib in sibs {
|
||||
match sib {
|
||||
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib3(sib3) => {
|
||||
let res_p: u8 = sib3
|
||||
.cell_reselection_serving_freq_info
|
||||
.cell_reselection_priority
|
||||
.0;
|
||||
if Some(res_p) > self.lte_priority {
|
||||
self.lte_priority = Some(res_p);
|
||||
debug!("set priority {} due to sib3 (frame {})", res_p, _packet_num);
|
||||
}
|
||||
}
|
||||
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib5(sib5) => {
|
||||
let carrier_freq_list = &sib5.inter_freq_carrier_freq_list;
|
||||
for carrier_freq in &carrier_freq_list.0 {
|
||||
if let Some(res_p) = &carrier_freq.cell_reselection_priority {
|
||||
let pri: u8 = res_p.0;
|
||||
if Some(pri) > self.lte_priority {
|
||||
self.lte_priority = Some(pri);
|
||||
debug!("set priority {} due to sib5 (frame {})", pri, _packet_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib6(sib6) => {
|
||||
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_fdd.as_ref() {
|
||||
for carrier_info in &carrier_info_list.0 {
|
||||
if let Some(CellReselectionPriority(p)) =
|
||||
carrier_info.cell_reselection_priority
|
||||
&& p == 0
|
||||
&& Some(p) > self.legacy_priority
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::High,
|
||||
message:
|
||||
"LTE cell advertised a 3G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
});
|
||||
self.legacy_priority = Some(p);
|
||||
debug!(
|
||||
"set legacy priority {} due to sib6 (frame {})",
|
||||
p, _packet_num
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,14 +146,13 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
for carrier_info in &carrier_info_list.0 {
|
||||
if let Some(CellReselectionPriority(p)) =
|
||||
carrier_info.cell_reselection_priority
|
||||
&& p == 0
|
||||
&& Some(p) > self.legacy_priority
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::High,
|
||||
message:
|
||||
"LTE cell advertised a 3G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
});
|
||||
self.legacy_priority = Some(p);
|
||||
debug!(
|
||||
"set legacy priority {} due to sib6 (frame {})",
|
||||
p, _packet_num
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,13 +166,13 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
for carrier_info in &carrier_info_list.0 {
|
||||
if let Some(CellReselectionPriority(p)) =
|
||||
carrier_info.common_info.cell_reselection_priority
|
||||
&& p == 0
|
||||
&& Some(p) > self.legacy_priority
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "LTE cell advertised a 2G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
});
|
||||
self.legacy_priority = p.into();
|
||||
debug!(
|
||||
"set legacy priority {} due to sib7 (frame {})",
|
||||
p, _packet_num
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ impl Analyzer for TestAnalyzer {
|
||||
.0
|
||||
.as_bitslice()
|
||||
.load_be::<u32>();
|
||||
let tac = sib1
|
||||
.cell_access_related_info
|
||||
.tracking_area_code
|
||||
.0
|
||||
.as_bitslice()
|
||||
.load_be::<u32>();
|
||||
let plmn = &sib1.cell_access_related_info.plmn_identity_list.0;
|
||||
let mcc_string: String;
|
||||
|
||||
@@ -62,8 +68,8 @@ impl Analyzer for TestAnalyzer {
|
||||
return Some(Event {
|
||||
event_type: EventType::Low,
|
||||
message: format!(
|
||||
"SIB1 received CID: {}, PLMN: {}-{}",
|
||||
cid, mcc_string, mnc_string
|
||||
"SIB1 received CID: {}, TAC: {}, PLMN: {}-{}",
|
||||
cid, tac, mcc_string, mnc_string
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
25
lib/src/clock.rs
Normal file
25
lib/src/clock.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! Global clock offset for adjusting timestamps.
|
||||
//!
|
||||
//! This module provides a global clock offset that can be used to adjust
|
||||
//! timestamps when the device's system clock is incorrect. The offset is
|
||||
//! stored in memory and is not persisted across restarts.
|
||||
|
||||
use chrono::{DateTime, Local, TimeDelta};
|
||||
use std::sync::RwLock;
|
||||
|
||||
static CLOCK_OFFSET: RwLock<TimeDelta> = RwLock::new(TimeDelta::zero());
|
||||
|
||||
/// Get the current clock offset.
|
||||
fn get_offset() -> TimeDelta {
|
||||
*CLOCK_OFFSET.read().unwrap()
|
||||
}
|
||||
|
||||
/// Set the clock offset.
|
||||
pub fn set_offset(offset: TimeDelta) {
|
||||
*CLOCK_OFFSET.write().unwrap() = offset;
|
||||
}
|
||||
|
||||
/// Get the current adjusted time (system time + offset).
|
||||
pub fn get_adjusted_now() -> DateTime<Local> {
|
||||
Local::now() + get_offset()
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Initialize logging with the given default level, suppressing noisy warnings
|
||||
/// from hampi about undecoded ASN1 extensions. Respects `RUST_LOG` overrides.
|
||||
pub fn init_logging(default_level: log::LevelFilter) {
|
||||
env_logger::Builder::new()
|
||||
.filter_level(default_level)
|
||||
//Filter out a stupid massive amount of uneccessary warnings from hampi about undecoded extensions
|
||||
.filter_module("asn1_codecs", log::LevelFilter::Error)
|
||||
.parse_default_env()
|
||||
.init();
|
||||
}
|
||||
|
||||
pub mod analysis;
|
||||
pub mod clock;
|
||||
pub mod diag;
|
||||
pub mod gsmtap;
|
||||
pub mod gsmtap_parser;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user