Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01915a22a6 |
@@ -1,11 +0,0 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
# RSA Marvin Attack in `rsa`, dragged in through rustcrypto (dev builds)
|
||||
# and adb_client (USB signing only, unrelated to marvin attack which
|
||||
# targets decryption).
|
||||
"RUSTSEC-2023-0071",
|
||||
# paste crate being unmaintained is not important. it's not dealing with
|
||||
# user-input. we could get rid of this warning by disabling the image
|
||||
# dependency in adb-client.
|
||||
"RUSTSEC-2024-0436",
|
||||
]
|
||||
@@ -5,10 +5,6 @@ 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
@@ -1,7 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
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
@@ -1,8 +1,7 @@
|
||||
## 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](https://github.com/EFForg/rayhunter/blob/main/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](../CONTRIBUTING.md) has been read
|
||||
|
||||
266
.github/workflows/main.yml
vendored
@@ -20,70 +20,49 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
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' }}
|
||||
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 }}
|
||||
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: detect file changes
|
||||
id: files_changed
|
||||
run: |
|
||||
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
|
||||
|
||||
# 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'
|
||||
# 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
|
||||
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"
|
||||
else
|
||||
echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^installer -e ^check -e ^lib -e ^rootshell -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
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 "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 == 'true'
|
||||
if: needs.files_changed.outputs.docs_changed != '0'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install mdBook
|
||||
run: |
|
||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
@@ -101,8 +80,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install mdBook
|
||||
run: |
|
||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
@@ -121,14 +98,12 @@ jobs:
|
||||
|
||||
check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.code_changed == 'true'
|
||||
if: needs.files_changed.outputs.code_changed != '0'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
@@ -149,37 +124,9 @@ jobs:
|
||||
run: |
|
||||
NO_FIRMWARE_BIN=true cargo clippy --verbose
|
||||
|
||||
installer_gui_check:
|
||||
# we test the GUI installer separately to:
|
||||
# 1) mimic the default behavior of cargo commands for rayhunter devs where
|
||||
# installer-gui isn't one of the default workspace packages
|
||||
# 2) avoid slowing down development on changes unrelated to the GUI installer
|
||||
test_web_frontend:
|
||||
needs: files_changed
|
||||
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
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# we don't need to run cargo fmt here because both cargo fmt and cargo
|
||||
# fmt --all runs on all workspace packages so this is handled by
|
||||
# check_and_test above
|
||||
- name: Check
|
||||
run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose
|
||||
- name: Run clippy
|
||||
run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose
|
||||
|
||||
test_daemon_frontend:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.web_changed == 'true'
|
||||
if: needs.files_changed.outputs.web_changed != '0'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -188,40 +135,19 @@ jobs:
|
||||
working-directory: daemon/web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm run check
|
||||
- run: npm run test
|
||||
|
||||
test_installer_frontend:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: installer-gui
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm run check
|
||||
|
||||
windows_installer_check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_changed == 'true'
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: cargo check
|
||||
shell: bash
|
||||
@@ -235,7 +161,7 @@ jobs:
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
|
||||
|
||||
build_rayhunter_check:
|
||||
if: needs.files_changed.outputs.daemon_changed == 'true'
|
||||
if: needs.files_changed.outputs.daemon_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -266,8 +192,6 @@ jobs:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform.target }}
|
||||
@@ -281,7 +205,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rootshell:
|
||||
if: needs.files_changed.outputs.rootshell_needed == 'true'
|
||||
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -290,8 +214,6 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
@@ -305,7 +227,10 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rayhunter:
|
||||
if: needs.files_changed.outputs.daemon_needed == 'true'
|
||||
# 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'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -315,8 +240,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
@@ -345,7 +268,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rust_installer:
|
||||
if: needs.files_changed.outputs.installer_changed == 'true'
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -378,8 +301,6 @@ jobs:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -392,145 +313,6 @@ jobs:
|
||||
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_linux:
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- build_rayhunter
|
||||
- build_rootshell
|
||||
- files_changed
|
||||
- installer_gui_check
|
||||
- test_installer_frontend
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
# we want to use the oldest supported version of ubuntu here to
|
||||
# maximize compatibility with older versions of glibc
|
||||
- name: linux-x64
|
||||
os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: linux-aarch64
|
||||
os: ubuntu-22.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install tauri dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev xdg-utils
|
||||
- name: Build GUI installer
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer-gui
|
||||
npm install
|
||||
npm run tauri build -- --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-${{ matrix.platform.name }}-appimage
|
||||
path: target/${{ matrix.platform.target }}/release/bundle/appimage/*.AppImage
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-${{ matrix.platform.name }}-deb
|
||||
path: target/${{ matrix.platform.target }}/release/bundle/deb/*.deb
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-${{ matrix.platform.name }}-rpm
|
||||
path: target/${{ matrix.platform.target }}/release/bundle/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_macos:
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- build_rayhunter
|
||||
- build_rootshell
|
||||
- files_changed
|
||||
- installer_gui_check
|
||||
- test_installer_frontend
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- name: macos-arm
|
||||
target: aarch64-apple-darwin
|
||||
- name: macos-intel
|
||||
target: x86_64-apple-darwin
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build GUI installer
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer-gui
|
||||
npm install
|
||||
npm run tauri build -- --target ${{ matrix.platform.target }}
|
||||
cd ..
|
||||
mv "target/${{ matrix.platform.target }}/release/bundle/macos/"*.app .
|
||||
zip -r "rayhunter-installer-${{ matrix.platform.name }}.app.zip" ./*.app
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-${{ matrix.platform.name }}-app
|
||||
path: ./*.app.zip
|
||||
if-no-files-found: error
|
||||
|
||||
build_installer_gui_windows:
|
||||
if: needs.files_changed.outputs.installer_gui_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- build_rayhunter
|
||||
- build_rootshell
|
||||
- files_changed
|
||||
- installer_gui_check
|
||||
- test_installer_frontend
|
||||
env:
|
||||
TARGET: x86_64-pc-windows-msvc
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ env.TARGET }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build GUI installer
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer-gui
|
||||
npm install
|
||||
npm run tauri build -- --target ${{ env.TARGET }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-msi
|
||||
path: target/${{ env.TARGET }}/release/bundle/msi/*.msi
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gui-installer-exe
|
||||
path: target/${{ env.TARGET }}/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build_release_zip:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -552,8 +334,6 @@ jobs:
|
||||
- windows-x86_64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Fix executable permissions on binaries
|
||||
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
|
||||
@@ -563,7 +343,7 @@ jobs:
|
||||
- name: Setup versioned release directory
|
||||
run: |
|
||||
platform="${{ matrix.platform }}"
|
||||
dest="rayhunter-v${VERSION}-${{ matrix.platform }}"
|
||||
dest="rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}"
|
||||
mkdir "$dest"
|
||||
# Handle installer with proper extension for Windows
|
||||
if [ "$platform" = "windows-x86_64" ]; then
|
||||
@@ -571,7 +351,7 @@ jobs:
|
||||
else
|
||||
mv installer-$platform/installer "$dest"/installer
|
||||
fi
|
||||
cp -r rayhunter-check-* rayhunter-daemon dist/scripts "$dest"/
|
||||
cp -r rayhunter-check-* rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
|
||||
zip -r "$dest.zip" "$dest"
|
||||
sha256sum "$dest.zip" > "$dest.zip.sha256"
|
||||
|
||||
|
||||
8
.github/workflows/release.yml
vendored
@@ -14,12 +14,10 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Ensure all Cargo.toml files have the same version defined.
|
||||
run: |
|
||||
defined_versions=$(find lib check daemon installer installer-gui rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
||||
find lib check daemon installer installer-gui rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
||||
defined_versions=$(find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
||||
find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
||||
echo number of defined versions = $defined_versions
|
||||
if [ $defined_versions != "1" ]
|
||||
then
|
||||
@@ -43,8 +41,6 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Create release
|
||||
run: |
|
||||
|
||||
3538
Cargo.lock
generated
12
Cargo.toml
@@ -7,17 +7,5 @@ members = [
|
||||
"rootshell",
|
||||
"telcom-parser",
|
||||
"installer",
|
||||
"installer-gui/src-tauri",
|
||||
]
|
||||
# at least for now, let's keep installer-gui out of the list of default
|
||||
# packages. installer-gui is still experimental and requires many new packages
|
||||
# both from cargo and the underlying operating system
|
||||
default-members = [
|
||||
"lib",
|
||||
"daemon",
|
||||
"check",
|
||||
"rootshell",
|
||||
"telcom-parser",
|
||||
"installer",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||

|
||||
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts, it can [support some other devices as well](https://efforg.github.io/rayhunter/supported-devices.html).
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](https://efforg.github.io/rayhunter/supported-devices.html).
|
||||
It's also designed to be as easy to install and use as possible, regardless of your level of technical skills, and to minimize false positives.
|
||||
|
||||
→ Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.10.1"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,4 +10,5 @@ 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,7 +177,14 @@ async fn main() {
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
rayhunter::init_logging(level);
|
||||
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();
|
||||
|
||||
let harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
info!("Analyzers:");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.10.1"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
@@ -18,6 +18,7 @@ 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"
|
||||
@@ -32,3 +33,4 @@ anyhow = "1.0.98"
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
async-trait = "0.1.88"
|
||||
uds = { version = "0.4.2", features = ["tokio"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use log::{info, warn};
|
||||
use log::{error, info};
|
||||
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 status not supported for this device, disabling battery notifications");
|
||||
return;
|
||||
info!("Battery level function not supported for device");
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to get battery status: {e}");
|
||||
error!("Failed to get battery status: {e}");
|
||||
true
|
||||
}
|
||||
Ok(status) => status.level <= LOW_BATTERY_LEVEL,
|
||||
@@ -83,12 +83,8 @@ 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) => {
|
||||
warn!("Failed to get battery status: {e}");
|
||||
error!("Failed to get battery status: {e}");
|
||||
continue;
|
||||
}
|
||||
Ok(status) => status,
|
||||
|
||||
@@ -77,7 +77,6 @@ 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
|
||||
|
||||
@@ -178,7 +178,6 @@ pub fn update_ui(
|
||||
let display_level = config.ui_level;
|
||||
if display_level == 0 {
|
||||
info!("Invisible mode, not spawning UI.");
|
||||
return;
|
||||
}
|
||||
|
||||
let colorblind_mode = config.colorblind_mode;
|
||||
@@ -215,13 +214,9 @@ 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;
|
||||
@@ -229,13 +224,12 @@ pub fn update_ui(
|
||||
fb.draw_line(Color::Pink, 50).await;
|
||||
fb.draw_line(Color::Cyan, 25).await;
|
||||
}
|
||||
// this branch is for ui_level 1, which is also the default if an
|
||||
// this branch id 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, status_bar_height, pattern)
|
||||
.await;
|
||||
fb.draw_patterned_line(color, 2, pattern).await;
|
||||
tokio::time::sleep(Duration::from_millis(REFRESH_RATE)).await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,8 +22,7 @@ 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_time, get_zip, serve_static,
|
||||
set_config, set_time_offset, test_notification,
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_zip, serve_static, set_config,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
|
||||
@@ -69,9 +68,6 @@ 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))
|
||||
@@ -171,7 +167,7 @@ fn run_shutdown_thread(
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), RayhunterError> {
|
||||
rayhunter::init_logging(log::LevelFilter::Info);
|
||||
env_logger::init();
|
||||
|
||||
#[cfg(feature = "rustcrypto-tls")]
|
||||
{
|
||||
@@ -207,10 +203,6 @@ 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,18 +6,9 @@ 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,
|
||||
@@ -69,21 +60,6 @@ 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,
|
||||
@@ -149,15 +125,24 @@ pub fn run_notification_worker(
|
||||
}
|
||||
}
|
||||
|
||||
match send_notification(&http_client, &url, notification.message.clone()).await
|
||||
match http_client
|
||||
.post(&url)
|
||||
.body(notification.message.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
notification.last_sent = Some(Instant::now());
|
||||
notification.failed_since_last_success = 0;
|
||||
notification.needs_sending = false;
|
||||
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());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to send notification: {e}");
|
||||
error!("Failed to send notification to ntfy: {e}");
|
||||
notification.failed_since_last_success += 1;
|
||||
notification.last_attempt = Some(Instant::now());
|
||||
}
|
||||
@@ -177,205 +162,3 @@ 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 = rayhunter::clock::get_adjusted_now();
|
||||
let now = Local::now();
|
||||
let metadata = RuntimeMetadata::new();
|
||||
ManifestEntry {
|
||||
name: format!("{}", now.timestamp()),
|
||||
@@ -300,8 +300,7 @@ 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(rayhunter::clock::get_adjusted_now());
|
||||
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
||||
self.write_manifest().await
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ 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};
|
||||
@@ -138,76 +136,6 @@ 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>,
|
||||
|
||||
@@ -9,53 +9,10 @@ use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use log::error;
|
||||
use rayhunter::gsmtap_parser::get_cached_cell_info;
|
||||
use rayhunter::{Device, util::RuntimeMetadata};
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// LTE cell/signal information from DIAG measurements.
|
||||
/// All fields are optional since they may not be available on all devices
|
||||
/// or may not have been received yet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CellSignalInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rsrp_dbm: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rsrq_db: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rssi_dbm: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pci: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub earfcn: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get cell/signal information for devices that support DIAG.
|
||||
/// Returns None for devices without DIAG support or if no measurements available.
|
||||
pub fn get_cell_info(device: &Device) -> Option<CellSignalInfo> {
|
||||
match device {
|
||||
// Devices with DIAG support
|
||||
Device::Orbic | Device::Tplink | Device::Tmobile | Device::Wingtech => {
|
||||
let info = get_cached_cell_info();
|
||||
// Only return if we have at least some data
|
||||
if info.rsrp_dbm.is_some() || info.pci.is_some() {
|
||||
Some(CellSignalInfo {
|
||||
rsrp_dbm: info.rsrp_dbm,
|
||||
rsrq_db: info.rsrq_db,
|
||||
rssi_dbm: info.rssi_dbm,
|
||||
pci: info.pci,
|
||||
earfcn: info.earfcn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// Devices without DIAG support
|
||||
Device::Pinephone | Device::Uz801 => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SystemStats {
|
||||
pub disk_stats: DiskStats,
|
||||
@@ -63,8 +20,6 @@ pub struct SystemStats {
|
||||
pub runtime_metadata: RuntimeMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_status: Option<BatteryState>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cell_info: Option<CellSignalInfo>,
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
@@ -81,7 +36,6 @@ impl SystemStats {
|
||||
None
|
||||
}
|
||||
},
|
||||
cell_info: get_cell_info(device),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,17 +37,6 @@ 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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
32
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.49.5",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
@@ -1167,9 +1167,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.49.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.5.tgz",
|
||||
"integrity": "sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==",
|
||||
"version": "2.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.46.2.tgz",
|
||||
"integrity": "sha512-bGs473Gj4TwFf7dw6ZUwQI0ayaDb83E7G06QnYeNQC2DmAaktgFU2uB0tSfZVhpHqYH4o8GsLBkG3ZjThtmsIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -1179,7 +1179,7 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.3.2",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -1198,15 +1198,11 @@
|
||||
"@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
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2201,9 +2197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
|
||||
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2771,9 +2767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3045,9 +3041,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
|
||||
@@ -78,8 +78,7 @@
|
||||
<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. We'll not accept bug reports about them unless something else is going wrong
|
||||
(such as false-positives or definite false-negatives)
|
||||
problem.
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-auto text-left">
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
onclick,
|
||||
ariaLabel,
|
||||
errorMessage,
|
||||
jsonBody,
|
||||
}: {
|
||||
url: string;
|
||||
method?: string;
|
||||
@@ -24,7 +23,6 @@
|
||||
onclick?: () => void | Promise<void>;
|
||||
ariaLabel?: string;
|
||||
errorMessage?: string;
|
||||
jsonBody?: unknown;
|
||||
} = $props();
|
||||
|
||||
let is_requesting = $state(false);
|
||||
@@ -45,7 +43,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
async function handle_click() {
|
||||
async function handleClick() {
|
||||
if (is_disabled) return;
|
||||
|
||||
is_requesting = true;
|
||||
@@ -53,8 +51,7 @@
|
||||
await user_action_req(
|
||||
method,
|
||||
url,
|
||||
errorMessage ? errorMessage : 'Error performing action',
|
||||
jsonBody
|
||||
errorMessage ? errorMessage : 'Error performing action'
|
||||
);
|
||||
if (onclick) {
|
||||
await onclick();
|
||||
@@ -74,7 +71,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={handle_click}
|
||||
onclick={handleClick}
|
||||
disabled={is_disabled}
|
||||
aria-label={ariaLabel || label}
|
||||
>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<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,18 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { get_config, set_config, test_notification, type Config } from '../utils.svelte';
|
||||
import { get_config, set_config, 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 load_config() {
|
||||
async function loadConfig() {
|
||||
try {
|
||||
loading = true;
|
||||
config = await get_config();
|
||||
@@ -26,7 +23,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function save_config() {
|
||||
async function saveConfig() {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
@@ -43,25 +40,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Load config when first shown
|
||||
$effect(() => {
|
||||
if (showConfig && !config) {
|
||||
load_config();
|
||||
loadConfig();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -91,7 +73,7 @@
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
save_config();
|
||||
saveConfig();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
@@ -107,12 +89,7 @@
|
||||
<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>
|
||||
@@ -161,49 +138,6 @@
|
||||
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">
|
||||
@@ -335,20 +269,6 @@
|
||||
Test Heuristic (noisy!)
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="diagnostic_analyzer"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.diagnostic_analyzer}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
for="diagnostic_analyzer"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Diagnostic Analyzer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
name: string;
|
||||
} = $props();
|
||||
|
||||
function confirm_delete() {
|
||||
function confirmDelete() {
|
||||
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={confirm_delete}
|
||||
onclick={confirmDelete}
|
||||
aria-label="delete"
|
||||
>
|
||||
<p>{text}</p>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
analysis_status === AnalysisStatus.Queued || analysis_status === AnalysisStatus.Running
|
||||
);
|
||||
|
||||
async function handle_re_analyze() {
|
||||
async function handleReAnalyze() {
|
||||
// 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={handle_re_analyze}
|
||||
onclick={handleReAnalyze}
|
||||
ariaLabel="re-analyze"
|
||||
errorMessage="Error re-analyzing recoding"
|
||||
>
|
||||
|
||||
@@ -33,23 +33,6 @@
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
// Cell signal quality assessment based on RSRP
|
||||
// Excellent: >= -80 dBm, Good: -80 to -90, Fair: -90 to -100, Poor: < -100
|
||||
let signal_quality = $derived.by(() => {
|
||||
const rsrp = stats.cell_info?.rsrp_dbm;
|
||||
if (rsrp === undefined) return { label: 'Unknown', color: 'text-gray-500' };
|
||||
if (rsrp >= -80) return { label: 'Excellent', color: 'text-green-600' };
|
||||
if (rsrp >= -90) return { label: 'Good', color: 'text-green-600' };
|
||||
if (rsrp >= -100) return { label: 'Fair', color: 'text-yellow-600' };
|
||||
return { label: 'Poor', color: 'text-red-600' };
|
||||
});
|
||||
|
||||
// Format signal value with unit
|
||||
function format_signal(value: number | undefined, unit: string): string {
|
||||
if (value === undefined) return '—';
|
||||
return `${value.toFixed(1)} ${unit}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -133,46 +116,6 @@
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
{#if stats.cell_info}
|
||||
<tr class="border-b">
|
||||
<th class={table_cell_classes}> Cell Signal </th>
|
||||
<td class={table_cell_classes}>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium {signal_quality.color}">
|
||||
{signal_quality.label}
|
||||
</span>
|
||||
{#if stats.cell_info.rsrp_dbm !== undefined}
|
||||
<span class="text-gray-600">
|
||||
(RSRP: {format_signal(stats.cell_info.rsrp_dbm, 'dBm')})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-xs">
|
||||
{#if stats.cell_info.pci !== undefined}
|
||||
<span>PCI: {stats.cell_info.pci}</span>
|
||||
{/if}
|
||||
{#if stats.cell_info.earfcn !== undefined}
|
||||
<span class="ml-2">EARFCN: {stats.cell_info.earfcn}</span>
|
||||
{/if}
|
||||
{#if stats.cell_info.rsrq_db !== undefined}
|
||||
<span class="ml-2"
|
||||
>RSRQ: {format_signal(stats.cell_info.rsrq_db, 'dB')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if stats.cell_info.rssi_dbm !== undefined}
|
||||
<span class="ml-2"
|
||||
>RSSI: {format_signal(
|
||||
stats.cell_info.rssi_dbm,
|
||||
'dBm'
|
||||
)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { breakpoints } from '../../theme';
|
||||
type Breakpoint = keyof typeof breakpoints;
|
||||
|
||||
// Store that tracks if a specific breakpoint matches
|
||||
export function create_breakpoint_store(breakpoint: Breakpoint): Readable<boolean> {
|
||||
export function createBreakpointStore(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 create_breakpoint_store(breakpoint: Breakpoint): Readable<boolea
|
||||
}
|
||||
|
||||
// Create stores for each breakpoint
|
||||
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');
|
||||
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');
|
||||
|
||||
@@ -3,7 +3,6 @@ export interface SystemStats {
|
||||
memory_stats: MemoryStats;
|
||||
runtime_metadata: RuntimeMetadata;
|
||||
battery_status?: BatteryStatus;
|
||||
cell_info?: CellSignalInfo;
|
||||
}
|
||||
|
||||
export interface RuntimeMetadata {
|
||||
@@ -31,11 +30,3 @@ export interface BatteryStatus {
|
||||
level: number;
|
||||
is_plugged_in: boolean;
|
||||
}
|
||||
|
||||
export interface CellSignalInfo {
|
||||
rsrp_dbm?: number;
|
||||
rsrq_db?: number;
|
||||
rssi_dbm?: number;
|
||||
pci?: number;
|
||||
earfcn?: number;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface AnalyzerConfig {
|
||||
nas_null_cipher: boolean;
|
||||
incomplete_sib: boolean;
|
||||
test_analyzer: boolean;
|
||||
diagnostic_analyzer: boolean;
|
||||
}
|
||||
|
||||
export enum enabled_notifications {
|
||||
@@ -27,18 +26,15 @@ export interface Config {
|
||||
analyzers: AnalyzerConfig;
|
||||
}
|
||||
|
||||
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();
|
||||
export async function req(method: string, url: string): Promise<string> {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
});
|
||||
const body = await response.text();
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return responseBody;
|
||||
return body;
|
||||
} else {
|
||||
throw new Error(responseBody);
|
||||
throw new Error(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +42,13 @@ export async function req(method: string, url: string, json_body?: unknown): Pro
|
||||
export async function user_action_req(
|
||||
method: string,
|
||||
url: string,
|
||||
error_msg: string,
|
||||
json_body?: unknown
|
||||
error_msg: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
return await req(method, url, json_body);
|
||||
return await req(method, url);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('beeeo');
|
||||
add_error(error, error_msg);
|
||||
}
|
||||
return undefined;
|
||||
@@ -90,24 +86,3 @@ 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,7 +10,6 @@
|
||||
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();
|
||||
@@ -103,7 +102,6 @@
|
||||
/>
|
||||
</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"
|
||||
@@ -150,26 +148,6 @@
|
||||
/>
|
||||
</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">
|
||||
@@ -208,7 +186,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
<ActionErrors />
|
||||
<ClockDriftAlert />
|
||||
{#if loaded}
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
{#if current_entry}
|
||||
|
||||
2125
daemon/web/yarn.lock
Normal file
2
dist/config.toml.in
vendored
@@ -12,7 +12,6 @@ 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
|
||||
@@ -39,4 +38,3 @@ null_cipher = true
|
||||
nas_null_cipher = true
|
||||
incomplete_sib = true
|
||||
test_analyzer = false
|
||||
diagnostic_analyzer = true
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# 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 the latest release (Windows)](./installing-from-release-windows.md)
|
||||
- [Installing from source](./installing-from-source.md)
|
||||
- [Updating Rayhunter](./updating-rayhunter.md)
|
||||
- [Configuration](./configuration.md)
|
||||
@@ -22,3 +21,5 @@
|
||||
- [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,8 +9,7 @@ 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* colored line.
|
||||
- *High visibility (full screen color)*: fills the entire screen with the status color (green for recording, red for warnings, white for paused).
|
||||
- *EFF logo*, which shows EFF logo and *and* colored line.
|
||||
- **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
@@ -24,27 +24,18 @@ 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?
|
||||
|
||||
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:
|
||||
|
||||
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
||||
|
||||
```sh
|
||||
# inside of Orbic's shell:
|
||||
echo 9 > /usrdata/mode.cfg
|
||||
reboot
|
||||
./installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
```
|
||||
|
||||
Make sure USB tethering is also enabled in the Orbic's UI.
|
||||
|
||||
To disable tethering again:
|
||||
|
||||
```sh
|
||||
# inside of Orbic's shell:
|
||||
echo 3 > /usrdata/mode.cfg
|
||||
reboot
|
||||
./installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
```
|
||||
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
@@ -52,16 +43,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, [first obtain a shell](./orbic.md#shell), then:
|
||||
To disable both WiFi bands:
|
||||
|
||||
```sh
|
||||
# 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
|
||||
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"
|
||||
```
|
||||
|
||||
To re-enable WiFi:
|
||||
|
||||
```sh
|
||||
# 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
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -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 (v2)
|
||||
### LTE SIB6/7 Downgrade
|
||||
|
||||
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 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.)
|
||||
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.
|
||||
|
||||
### Null Cipher
|
||||
|
||||
@@ -73,9 +73,6 @@ This analyzer tests whether the SIB1 message contains a complete SIB chain (SIB3
|
||||
|
||||
On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **IMSI Requested** detection it should be considered as a strong indicator of malicious activity.
|
||||
|
||||
### Diagnostic Information
|
||||
This analyzer displays some diagnostic information about when your device connects and disconnects from certain towers. It is helpful for analysis of suspicious PCAPs. The informational warnings in here can safely be ignored until there is a low, medium, or high severity warning.
|
||||
|
||||
### Test Analyzer
|
||||
|
||||
This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisy so we do not recommend leaving it on but if this alerts it means your Rayhunter device is working!
|
||||
|
||||
@@ -3,8 +3,5 @@
|
||||
So, you've got one of the [supported devices](./supported-devices.md), and are ready to start catching IMSI catchers. You have two options for installing Rayhunter:
|
||||
|
||||
* [installing from a release (recommended)](./installing-from-release.md)
|
||||
* [installing from a release on Windows](./installing-from-release-windows.md)
|
||||
* [installing from source](./installing-from-source.md)
|
||||
|
||||
Already have Rayhunter installed but looking to update?
|
||||
|
||||
* [Updating Rayhunter](./updating-rayhunter.md)
|
||||
@@ -41,9 +41,6 @@ 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
|
||||
|
||||
@@ -51,8 +48,7 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
./installer tplink
|
||||
```
|
||||
|
||||
* 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 Verizon Orbic, the password is the WiFi password.
|
||||
* 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,11 +47,12 @@ cargo build-daemon-firmware-devel
|
||||
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
|
||||
|
||||
# Build rootshell
|
||||
cargo build-rootshell-firmware-devel
|
||||
cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
|
||||
|
||||
# Replace 'orbic' with your device type if different.
|
||||
# A list of possible values can be found with 'cargo run --bin installer help'.
|
||||
FIRMWARE_PROFILE=firmware-devel cargo run -p installer --bin installer orbic
|
||||
# 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
|
||||
```
|
||||
|
||||
### If you're on Windows or can't run the install scripts
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
# LTE ML1 Serving Cell Measurement (0xB193)
|
||||
|
||||
This document describes the Qualcomm DIAG log code 0xB193 (LTE ML1 Serving Cell Measurement Response), which provides detailed LTE signal strength measurements including RSRP, RSRQ, and RSSI.
|
||||
|
||||
## Overview
|
||||
|
||||
Log code 0xB193 (`LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE`) is emitted by the Qualcomm modem's Layer 1 (ML1) component and contains periodic measurements of the serving cell's signal characteristics. Rayhunter captures these measurements and includes the RSRP value in GSMTAP headers for PCAP output.
|
||||
|
||||
## Packet Structure
|
||||
|
||||
The 0xB193 log uses a subpacket architecture common to many Qualcomm DIAG logs:
|
||||
|
||||
```
|
||||
+------------------+
|
||||
| Main Header | 4 bytes
|
||||
+------------------+
|
||||
| Subpacket Header | 4 bytes
|
||||
+------------------+
|
||||
| Subpacket Data | Variable (version-dependent)
|
||||
+------------------+
|
||||
```
|
||||
|
||||
### Main Header (4 bytes)
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-----------------|---------------------------------------|
|
||||
| 0 | 1 | main_version | Main packet version (observed: 1) |
|
||||
| 1 | 1 | num_subpackets | Number of subpackets (typically 1) |
|
||||
| 2 | 2 | reserved | Reserved/padding |
|
||||
|
||||
### Subpacket Header (4 bytes)
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-------------------|-------------------------------------|
|
||||
| 0 | 1 | subpacket_id | Subpacket identifier |
|
||||
| 1 | 1 | subpacket_version | Subpacket version (see below) |
|
||||
| 2 | 2 | subpacket_size | Size of subpacket including header |
|
||||
|
||||
### Known Subpacket Versions
|
||||
|
||||
Different modem firmware versions emit different subpacket versions. The field offsets within the subpacket data vary by version:
|
||||
|
||||
| Version | PCI Offset | EARFCN Offset | RSRP Offset | Notes |
|
||||
|---------|------------|---------------|-------------|--------------------------|
|
||||
| 4 | 0 | 2 | 12 | Early format (SCAT) |
|
||||
| 7 | 0 | 4 | 14 | Intermediate format |
|
||||
| 18-24 | 0 | 4 | 24 | Common on Orbic RC400L |
|
||||
| 35-40 | 0 | 4 | 28 | Newer modems |
|
||||
|
||||
The Orbic RC400L device used for development emits **subpacket version 18**.
|
||||
|
||||
## Signal Measurement Fields
|
||||
|
||||
### RSRP (Reference Signal Received Power)
|
||||
|
||||
RSRP is the primary signal strength indicator for LTE. The raw 12-bit value is converted to dBm:
|
||||
|
||||
```
|
||||
RSRP (dBm) = -180.0 + (raw_value & 0xFFF) * 0.0625
|
||||
```
|
||||
|
||||
Typical range: -140 dBm (very weak) to -44 dBm (very strong)
|
||||
|
||||
### PCI (Physical Cell ID)
|
||||
|
||||
The Physical Cell ID identifies the serving cell. Stored as a 16-bit little-endian value at the PCI offset.
|
||||
|
||||
Range: 0-503
|
||||
|
||||
### EARFCN (E-UTRA Absolute Radio Frequency Channel Number)
|
||||
|
||||
The EARFCN identifies the carrier frequency. Stored as a 32-bit little-endian value at the EARFCN offset.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Caching Strategy**: Since 0xB193 messages arrive independently from RRC OTA messages, rayhunter caches the most recent RSRP value and applies it to subsequent GSMTAP headers.
|
||||
|
||||
2. **Signal Conversion**: The `signal_dbm` field in GSMTAP headers is an `i8`, so the RSRP value is clamped to the range -128 to 0 dBm.
|
||||
|
||||
3. **Version Detection**: The subpacket version determines field offsets. Unknown versions fall back to the v7 layout.
|
||||
|
||||
## References
|
||||
|
||||
### SCAT (Signaling Collection and Analysis Tool)
|
||||
|
||||
The [SCAT project](https://github.com/fgsect/scat) by the Firmware Security (fgsect) research group at TU Berlin provides Qualcomm DIAG log parsers.
|
||||
|
||||
Relevant file: `parsers/qualcomm/diagltelogparser.py`
|
||||
|
||||
```python
|
||||
# SCAT v4/v5 parser structure (simplified)
|
||||
# pci = struct.unpack('<H', payload[0:2])
|
||||
# earfcn = struct.unpack('<H', payload[2:4]) # or <L for 32-bit
|
||||
# rsrp_raw = struct.unpack('<L', payload[offset:offset+4])
|
||||
```
|
||||
|
||||
### Mobile Insight
|
||||
|
||||
The [Mobile Insight project](https://github.com/mobile-insight/mobileinsight-core) from UCLA WiNG Lab provides comprehensive Qualcomm DIAG parsing with extensive version support.
|
||||
|
||||
Relevant file: `mobile_insight/analyzer/msg_logger.py` and related LTE analyzers
|
||||
|
||||
Mobile Insight documents subpacket versions 4, 7, 18, 19, 22, 24, 35, 36, and 40, with version-specific field layouts.
|
||||
|
||||
### QCSuper
|
||||
|
||||
The [QCSuper project](https://github.com/P1sec/QCSuper) by P1 Security provides another implementation of Qualcomm DIAG protocol handling.
|
||||
|
||||
### 3GPP Specifications
|
||||
|
||||
- **3GPP TS 36.214**: Physical layer measurements (defines RSRP, RSRQ, RSSI)
|
||||
- **3GPP TS 36.133**: Requirements for support of radio resource management
|
||||
|
||||
## Example Output
|
||||
|
||||
When rayhunter captures a 0xB193 log, debug output shows:
|
||||
|
||||
```
|
||||
ML1 0xB193 v18: RSRP=-94.8dBm, PCI=446, EARFCN=975
|
||||
```
|
||||
|
||||
The corresponding GSMTAP packets in Wireshark will display `Signal dBm: -95` (rounded to i8).
|
||||
@@ -30,7 +30,7 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
|
||||
Connect to the hotspot's network using WiFi or USB tethering and run:
|
||||
|
||||
```sh
|
||||
./installer orbic --admin-password 'mypassword'
|
||||
./installer orbic-network --admin-password 'mypassword'
|
||||
```
|
||||
|
||||
The password (in place of `mypassword`) is under the battery.
|
||||
@@ -38,5 +38,5 @@ The password (in place of `mypassword`) is under the battery.
|
||||
## Obtaining a shell
|
||||
|
||||
```sh
|
||||
./installer util orbic-shell
|
||||
./installer util orbic-start-telnet
|
||||
```
|
||||
|
||||
@@ -6,8 +6,7 @@ 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). You should not
|
||||
pay more than 30 USD for such a device (without shipping).
|
||||
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
||||
|
||||
[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.
|
||||
|
||||
@@ -38,11 +37,12 @@ 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
|
||||
enabled. Instead you can use `./installer util orbic-shell`.
|
||||
enabled. Instead you can use `./installer util orbic-start-telnet` and connect
|
||||
to the hotspot using `nc 192.168.1.1 24`. On Windows you might not have `nc`
|
||||
and will have to use WSL for that.
|
||||
|
||||
If you are using an installer prior to 0.7.0 or `orbic-usb` explicitly, you can
|
||||
obtain a root shell by running `adb shell` or `./installer util shell`. Then,
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
|
||||
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, 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'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'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,7 +35,6 @@ 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
|
||||
|
||||
@@ -43,10 +42,11 @@ Follow the [release installation guide](./installing-from-release.md). Substitut
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
You can obtain a root shell with the following command:
|
||||
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
|
||||
|
||||
```sh
|
||||
./installer util tplink-shell
|
||||
./installer util tplink-start-telnet
|
||||
telnet 192.168.0.1
|
||||
```
|
||||
|
||||
## Display states
|
||||
@@ -70,7 +70,7 @@ On hardware revisions starting with v4.0, the installer will modify settings to
|
||||
add two port triggers. You can look at `Settings > NAT Settings > Port
|
||||
Triggers` in TP-Link's admin UI to see them.
|
||||
|
||||
1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-shell`.
|
||||
1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-start-telnet`.
|
||||
2. One port trigger "rayhunter-daemon" to auto-start Rayhunter on boot. If you remove this, Rayhunter will have to be started manually from shell.
|
||||
|
||||
## Other links
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
# Uninstalling
|
||||
|
||||
There is no automated uninstallation routine, so this page documents the routine for some devices.
|
||||
|
||||
## Orbic
|
||||
|
||||
Run `./installer util orbic-shell --admin-password mypassword`. Refer to the
|
||||
installation instructions for how to find out the admin password.
|
||||
To uninstall Rayhunter, power on your Orbic device and connect to it via USB. Then, start a rootshell on it by running `adb shell`, followed by `rootshell`.
|
||||
|
||||
Inside, run:
|
||||
Once in a rootshell, run:
|
||||
|
||||
```shell
|
||||
echo 3 > /usrdata/mode.cfg # only relevant if you previously installed via ADB installer
|
||||
echo 3 > /usrdata/mode.cfg
|
||||
rm -rf /data/rayhunter /etc/init.d/rayhunter_daemon /bin/rootshell
|
||||
reboot
|
||||
```
|
||||
|
||||
Your device is now Rayhunter-free, and should no longer be rooted.
|
||||
Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabled mode.
|
||||
|
||||
## TPLink
|
||||
|
||||
1. Run `./installer util tplink-shell` to obtain rootshell on the device.
|
||||
1. Run `./installer util tplink-start-telnet`
|
||||
2. Telnet into the device `telnet 192.168.0.1`
|
||||
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
|
||||
4. `update-rc.d rayhunter_daemon remove`
|
||||
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
|
||||
|
||||
11
installer-gui/.gitignore
vendored
@@ -1,11 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/src-tauri/binaries
|
||||
@@ -1 +0,0 @@
|
||||
package-lock.json
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# Rayhunter GUI Installer
|
||||
|
||||
This directory contains experimental work on a Rayhunter GUI installer based on [Tauri](https://tauri.app/).
|
||||
|
||||
## Dependencies
|
||||
|
||||
Before building the GUI installer, you'll first need to install its dependencies.
|
||||
|
||||
### Tauri Dependencies
|
||||
|
||||
You'll need to install [Tauri's dependencies](https://tauri.app/start/prerequisites/). In addition to Rust, you'll need [Node.js/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you're on Linux, also be sure to install the necessary [system dependencies](https://tauri.app/start/prerequisites/#linux) from your package manager.
|
||||
|
||||
### Rayhunter CLI Installer
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
cargo build-daemon-firmware-devel
|
||||
cargo build-rootshell-firmware-devel
|
||||
|
||||
(cd installer-gui && FIRMWARE_PROFILE=firmware-devel npm run tauri android build)
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
After preparing dependencies, the GUI installer can be built by:
|
||||
|
||||
1. Running `npm install` in this directory.
|
||||
2. Running `npm run tauri dev`.
|
||||
|
||||
This will build the GUI installer in development mode. While this command is running, any changes to either the frontend or backend code will cause the installer to be reloaded or rebuilt.
|
||||
|
||||
You can also run `npm run tauri build` to create the final GUI installer artifacts for your OS as is done in CI.
|
||||
@@ -1,42 +0,0 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
|
||||
export default ts.config(
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/**', 'dist/'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
}
|
||||
);
|
||||
4215
installer-gui/package-lock.json
generated
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "installer-gui",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"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",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"fix": "eslint --fix .",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"tailwindcss": "^4.1.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
3
installer-gui/src-tauri/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "installer-gui"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "installer_gui_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1.0.100"
|
||||
installer = { path = "../../installer" }
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "opener:default"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,34 +0,0 @@
|
||||
use tauri::Emitter;
|
||||
|
||||
async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
installer::run_with_callback(
|
||||
// TODO: we should split using something similar to shlex in python
|
||||
args.split_whitespace(),
|
||||
Some(Box::new(move |output| {
|
||||
app_handle
|
||||
.emit("installer-output", output)
|
||||
.expect("Error sending Rayhunter CLI installer output to GUI frontend");
|
||||
})),
|
||||
)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn install_rayhunter(app_handle: tauri::AppHandle, args: String) -> Result<(), String> {
|
||||
// the return value of tauri commands needs to be serializable by serde which we accomplish
|
||||
// here by converting anyhow::Error to a string
|
||||
run_installer(app_handle, args)
|
||||
.await
|
||||
.map_err(|error| format!("{error:?}"))
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![install_rayhunter])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
installer_gui_lib::run()
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Rayhunter Installer",
|
||||
"identifier": "com.rayhunter-installer.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Rayhunter Installer",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app", "appimage", "deb", "msi", "nsis", "rpm"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-rayhunter-blue: #4e4eb1;
|
||||
--color-rayhunter-dark-blue: #3f3da0;
|
||||
--color-rayhunter-green: #94ea18;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Tauri doesn't have a Node.js server to do proper SSR
|
||||
// so we will use adapter-static to prerender the app (SSG)
|
||||
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
@@ -1,95 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
let buttonEnabled = $state(true);
|
||||
let installerArgs = $state('');
|
||||
let installerOutput = $state('');
|
||||
|
||||
listen<string>('installer-output', (event) => {
|
||||
installerOutput += event.payload;
|
||||
});
|
||||
|
||||
async function run_installer(event: Event) {
|
||||
event.preventDefault();
|
||||
buttonEnabled = false;
|
||||
installerOutput = '';
|
||||
try {
|
||||
await invoke('install_rayhunter', { args: installerArgs });
|
||||
} catch (error) {
|
||||
installerOutput +=
|
||||
'Rayhunter GUI installer encountered an internal error. Error was:\n';
|
||||
installerOutput += error;
|
||||
}
|
||||
buttonEnabled = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
|
||||
<!-- https://www.w3.org/WAI/tutorials/images/decorative/ -->
|
||||
<img src="/rayhunter_text.png" alt="" class="h-10 xl:h-12" />
|
||||
<div class="flex flex-row gap-4">
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
href="https://github.com/EFForg/rayhunter/issues"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Report Issue</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
|
||||
fill-rule="evenodd"
|
||||
d="M12.006 2a9.847 9.847 0 0 0-6.484 2.44 10.32 10.32 0 0 0-3.393 6.17 10.48 10.48 0 0 0 1.317 6.955 10.045 10.045 0 0 0 5.4 4.418c.504.095.683-.223.683-.494 0-.245-.01-1.052-.014-1.908-2.78.62-3.366-1.21-3.366-1.21a2.711 2.711 0 0 0-1.11-1.5c-.907-.637.07-.621.07-.621.317.044.62.163.885.346.266.183.487.426.647.71.135.253.318.476.538.655a2.079 2.079 0 0 0 2.37.196c.045-.52.27-1.006.635-1.37-2.219-.259-4.554-1.138-4.554-5.07a4.022 4.022 0 0 1 1.031-2.75 3.77 3.77 0 0 1 .096-2.713s.839-.275 2.749 1.05a9.26 9.26 0 0 1 5.004 0c1.906-1.325 2.74-1.05 2.74-1.05.37.858.406 1.828.101 2.713a4.017 4.017 0 0 1 1.029 2.75c0 3.939-2.339 4.805-4.564 5.058a2.471 2.471 0 0 1 .679 1.897c0 1.372-.012 2.477-.012 2.814 0 .272.18.592.687.492a10.05 10.05 0 0 0 5.388-4.421 10.473 10.473 0 0 0 1.313-6.948 10.32 10.32 0 0 0-3.39-6.165A9.847 9.847 0 0 0 12.007 2Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
href="https://efforg.github.io/rayhunter/"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Docs</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="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 19V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v13H7a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h12M9 3v14m7 0v4"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<form class="flex justify-center pt-5" onsubmit={run_installer}>
|
||||
<input
|
||||
class="mr-1 px-5 py-2 rounded-lg shadow-md"
|
||||
placeholder="Enter CLI installer args..."
|
||||
bind:value={installerArgs}
|
||||
/>
|
||||
<button
|
||||
class="{buttonEnabled ? 'cursor-pointer' : ''} px-5 py-2 rounded-lg shadow-md"
|
||||
disabled={!buttonEnabled}
|
||||
type="submit">Run</button
|
||||
>
|
||||
</form>
|
||||
<p class="p-4">Installer output:</p>
|
||||
<p class="bg-gray-100 px-5 py-2 rounded-lg shadow-md whitespace-pre-line">
|
||||
{installerOutput}
|
||||
</p>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,15 +0,0 @@
|
||||
// Tauri doesn't have a Node.js server to do proper SSR
|
||||
// so we will use adapter-static to prerender the app (SSG)
|
||||
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [sveltekit(), tailwindcss()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: 'ws',
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,23 +1,15 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.10.1"
|
||||
version = "0.8.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "installer"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "installer"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
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.11.1"
|
||||
bytes = "1.10.1"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
hyper = "1.6.0"
|
||||
@@ -31,12 +23,8 @@ sha2 = "0.10.8"
|
||||
tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false }
|
||||
tokio-retry2 = "0.5.7"
|
||||
tokio-stream = "0.1.17"
|
||||
futures = "0.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
termios = "0.3"
|
||||
|
||||
[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client]
|
||||
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
default-features = false
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use core::str;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -24,7 +23,6 @@ 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 {}",
|
||||
@@ -33,5 +31,6 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
142
installer/install.ps1
Normal file
@@ -0,0 +1,142 @@
|
||||
$global:adb = ".\platform-tools-latest-windows\platform-tools\adb.exe"
|
||||
$global:serial = ".\installer-windows-x86_64\installer.exe"
|
||||
|
||||
function _adb_push {
|
||||
& $global:adb -d push @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
function _adb_shell {
|
||||
& $global:adb -d shell @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
function _wait_for_adb_shell {
|
||||
do {
|
||||
start-sleep -seconds 1
|
||||
$success = _adb_shell "uname -a"
|
||||
} until ($success -eq 0)
|
||||
}
|
||||
|
||||
function _wait_for_atfwd_daemon {
|
||||
do {
|
||||
start-sleep -seconds 1
|
||||
$success = _adb_shell "pgrep atfwd_daemon"
|
||||
} until ($success -eq 0)
|
||||
}
|
||||
|
||||
function force_debug_mode {
|
||||
write-host "Using adb at $($global:adb)"
|
||||
write-host "Forcing a switch into debug mode to enable ADB"
|
||||
_serial "--root" | Out-Host
|
||||
write-host "adb enabled, waiting for reboot..." -nonewline
|
||||
_wait_for_adb_shell
|
||||
write-host " it's alive!"
|
||||
write-host "waiting for atfwd_daemon to start ..." -nonewline
|
||||
_wait_for_atfwd_daemon
|
||||
write-host " done!"
|
||||
}
|
||||
function _serial {
|
||||
param (
|
||||
[Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Args
|
||||
)
|
||||
|
||||
# Build the full argument list
|
||||
$allArgs = @("util", "serial") + $Args
|
||||
|
||||
# Call the serial executable
|
||||
& $global:serial @allArgs
|
||||
}
|
||||
|
||||
function setup_rootshell {
|
||||
write-host "setting up rootshell"
|
||||
_adb_push "rootshell" "/tmp" | Out-null
|
||||
write-host "cp..."
|
||||
_serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
write-host "chown..."
|
||||
_serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
write-host "chmod..."
|
||||
_serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
_adb_shell '/bin/rootshell -c id' | Out-null
|
||||
write-host "we have root!"
|
||||
}
|
||||
|
||||
function setup_rayhunter {
|
||||
write-host "installing rayhunter..."
|
||||
_serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
|
||||
_adb_push "config.toml.in" "/tmp/config.toml" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
|
||||
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
|
||||
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
|
||||
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
|
||||
|
||||
_serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
|
||||
_serial "AT+SYSCMD=chmod 755 /etc/init.d/rayhunter_daemon" | Out-Host
|
||||
_serial "AT+SYSCMD=chmod 755 /etc/init.d/misc-daemon" | Out-Host
|
||||
|
||||
write-host "waiting for reboot..."
|
||||
_serial "AT+SYSCMD=shutdown -r -t 1 now" | Out-Host
|
||||
do {
|
||||
start-sleep -seconds 1
|
||||
} until ((_adb_shell "true 2> /dev/null") -ne 0)
|
||||
|
||||
_wait_for_adb_shell
|
||||
write-host "done!"
|
||||
}
|
||||
|
||||
function test_rayhunter {
|
||||
$URL = "http://localhost:8080/index.html"
|
||||
& $global:adb -d forward tcp:8080 tcp:8080
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
write-host "adb forward tcp:8080 tcp:8080 failed with exit code $($exitCode)"
|
||||
return
|
||||
}
|
||||
write-host "checking for rayhunter server..." -nonewline
|
||||
$seconds = 0
|
||||
do {
|
||||
try {
|
||||
$resp = invoke-webrequest -uri $URL
|
||||
} catch {
|
||||
# Fail silently
|
||||
$resp = $null
|
||||
}
|
||||
if ($resp.statuscode -eq 200) {
|
||||
write-host "success!"
|
||||
write-host "you can access rayhunter at $($URL)"
|
||||
return
|
||||
}
|
||||
start-sleep 1
|
||||
$seconds = $seconds + 1
|
||||
} until ($seconds -eq 30)
|
||||
write-host "timeout reached! failed to reach rayhunter url $($URL), something went wrong :("
|
||||
}
|
||||
|
||||
function get_android_tools {
|
||||
write-host "adb not found, downloading local copy"
|
||||
invoke-webrequest "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -outfile ./platform-tools-latest-windows.zip
|
||||
expand-archive -force -path "platform-tools-latest-windows.zip"
|
||||
}
|
||||
|
||||
if (-not (test-path -path $global:serial)) {
|
||||
write-error "can't find serial, aborting"
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (test-path -path $global:adb)) {
|
||||
get_android_tools
|
||||
}
|
||||
|
||||
force_debug_mode
|
||||
setup_rootshell
|
||||
setup_rayhunter
|
||||
test_rayhunter
|
||||
@@ -1,68 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
use anyhow::{Context, Error};
|
||||
use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use anyhow::bail;
|
||||
|
||||
mod connection;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod orbic;
|
||||
mod orbic_auth;
|
||||
mod orbic_network;
|
||||
mod output;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod pinephone;
|
||||
mod tmobile;
|
||||
mod tplink;
|
||||
mod util;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod uz801;
|
||||
mod wingtech;
|
||||
|
||||
use crate::output::eprintln;
|
||||
|
||||
static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
|
||||
static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
// A note on stylisation of device names: strip special characters and spell like This regardless
|
||||
// of the manufacturer's capitalisation.
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
OrbicUsb(InstallOrbic),
|
||||
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
|
||||
#[clap(alias = "orbic-network")]
|
||||
Orbic(OrbicNetworkArgs),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on the Uz801.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Uz801(Uz801Args),
|
||||
/// Install rayhunter on a PinePhone's Quectel modem.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Pinephone(InstallPinephone),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
Tplink(InstallTpLink),
|
||||
/// Install rayhunter on the Wingtech CT2MHS01.
|
||||
Wingtech(WingtechArgs),
|
||||
/// Developer utilities.
|
||||
Util(Util),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallTpLink {
|
||||
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
|
||||
/// that if an SD card is later added, your existing installation is shadowed!
|
||||
#[arg(long)]
|
||||
skip_sdcard: bool,
|
||||
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// For advanced users: Specify the path of the SD card to be mounted explicitly.
|
||||
///
|
||||
/// The default (empty string) is to use whichever sdcard path the device would use natively to
|
||||
/// mount storage on. On most TP-Link this is /media/card, but on hardware versions 9+ this is
|
||||
/// /media/sdcard
|
||||
///
|
||||
/// Only override this when the installer does not work on your hardware version, as otherwise
|
||||
/// 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 {
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct OrbicNetworkArgs {
|
||||
/// IP address for Orbic admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Admin username for authentication.
|
||||
#[arg(long, default_value = "admin")]
|
||||
admin_username: String,
|
||||
|
||||
/// 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)]
|
||||
struct InstallPinephone {}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Util {
|
||||
#[command(subcommand)]
|
||||
command: UtilSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum UtilSubCommand {
|
||||
/// Send a serial command to the Orbic.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Serial(Serial),
|
||||
/// Start an ADB shell
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Shell,
|
||||
/// Root the Tmobile and launch adb.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
TmobileStartAdb(TmobileArgs),
|
||||
/// Root the Tmobile and launch telnetd.
|
||||
TmobileStartTelnet(TmobileArgs),
|
||||
/// Root the Uz801 and launch adb.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Uz801StartAdb(Uz801Args),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
/// Root the TP-Link and open an interactive shell.
|
||||
TplinkShell(TplinkStartTelnet),
|
||||
/// Root the Wingtech and launch telnetd.
|
||||
WingtechStartTelnet(WingtechArgs),
|
||||
/// Root the Wingtech and launch adb.
|
||||
WingtechStartAdb(WingtechArgs),
|
||||
/// Unlock the Pinephone's modem and start adb.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
PinephoneStartAdb,
|
||||
/// Lock the Pinephone's modem and stop adb.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
PinephoneStopAdb,
|
||||
/// Root the Orbic and launch telnetd.
|
||||
OrbicStartTelnet(OrbicNetworkArgs),
|
||||
/// Root the Orbic and open an interactive shell.
|
||||
OrbicShell(OrbicNetworkArgs),
|
||||
/// Send a file to the TP-Link device over telnet.
|
||||
///
|
||||
/// Before running this utility, you need to make telnet accessible with `installer util
|
||||
/// tplink-start-telnet`.
|
||||
TplinkSendFile(TplinkSendFile),
|
||||
/// Send a file to the Wingtech device over telnet.
|
||||
///
|
||||
/// Before running this utility, you need to make telnet accessible with `installer util
|
||||
/// wingtech-start-telnet`.
|
||||
WingtechSendFile(WingtechSendFile),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TmobileArgs {
|
||||
/// IP address for Tmobile admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Uz801Args {
|
||||
/// IP address for Uz801 admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.100.1")]
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TplinkStartTelnet {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TplinkSendFile {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
/// Local path to the file to send.
|
||||
local_path: String,
|
||||
/// Remote path where the file should be stored on the device.
|
||||
remote_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechSendFile {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
/// Local path to the file to send.
|
||||
local_path: String,
|
||||
/// Remote path where the file should be stored on the device.
|
||||
remote_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechArgs {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Serial {
|
||||
#[arg(long)]
|
||||
root: bool,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
async fn run(args: Args) -> Result<(), Error> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
|
||||
|
||||
match args.command {
|
||||
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?,
|
||||
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
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 {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
if serial_cmd.root {
|
||||
if !serial_cmd.command.is_empty() {
|
||||
eprintln!("You cannot use --root and specify a command at the same time");
|
||||
std::process::exit(64);
|
||||
}
|
||||
orbic::enable_command_mode()?;
|
||||
} else if serial_cmd.command.is_empty() {
|
||||
eprintln!("Command cannot be an empty string");
|
||||
std::process::exit(64);
|
||||
} else {
|
||||
let cmd = serial_cmd.command.join(" ");
|
||||
match orbic::open_orbic()? {
|
||||
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
|
||||
None => bail!(orbic::ORBIC_NOT_FOUND),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
UtilSubCommand::TplinkShell(options) => {
|
||||
tplink::shell(&options.admin_ip).await.context("\nFailed to open shell on TP-Link device")?;
|
||||
}
|
||||
UtilSubCommand::TplinkSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
||||
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to start telnet on the Orbic RC400L")?,
|
||||
UtilSubCommand::OrbicShell(args) => orbic_network::shell(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Type alias for output callback function
|
||||
pub type OutputCallback = Box<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
/// Run the installer with CLI arguments and optional output callback
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use installer;
|
||||
///
|
||||
/// // if the callback is None, stdout/stderr is going to be used
|
||||
/// let result = installer::run_with_callback(
|
||||
/// ["orbic-network", "--admin-password", "12345"],
|
||||
/// Some(Box::new(|output| {
|
||||
/// print!("{}", output);
|
||||
/// }))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn run_with_callback<'a>(
|
||||
args: impl IntoIterator<Item = &'a str>,
|
||||
callback: Option<OutputCallback>,
|
||||
) -> Result<(), Error> {
|
||||
let _guard;
|
||||
if let Some(cb) = callback {
|
||||
_guard = output::set_output_callback(move |s: &str| cb(s));
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to create Tokio runtime")?
|
||||
.block_on(async {
|
||||
let args = std::iter::once("installer").chain(args);
|
||||
match Args::try_parse_from(args) {
|
||||
Ok(parsed_args) => run(parsed_args).await,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the version of the installer
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Run the CLI installer
|
||||
///
|
||||
/// This function is public so the binary can call it, library users should use `run_with_callback`
|
||||
/// instead.
|
||||
pub async fn main_cli() -> Result<(), Error> {
|
||||
let args = Args::parse();
|
||||
run(args).await
|
||||
}
|
||||
@@ -1,6 +1,260 @@
|
||||
use anyhow::{Context, Error, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod orbic_auth;
|
||||
mod orbic_network;
|
||||
mod pinephone;
|
||||
mod tmobile;
|
||||
mod tplink;
|
||||
mod util;
|
||||
mod uz801;
|
||||
mod wingtech;
|
||||
|
||||
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
|
||||
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
// A note on stylisation of device names: strip special characters and spell like This regardless
|
||||
// of the manufacturer's capitalisation.
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
|
||||
OrbicUsb(InstallOrbic),
|
||||
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
|
||||
#[clap(alias = "orbic-network")]
|
||||
Orbic(OrbicNetworkArgs),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on the Uz801.
|
||||
Uz801(Uz801Args),
|
||||
/// Install rayhunter on a PinePhone's Quectel modem.
|
||||
Pinephone(InstallPinephone),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
Tplink(InstallTpLink),
|
||||
/// Install rayhunter on the Wingtech CT2MHS01.
|
||||
Wingtech(WingtechArgs),
|
||||
/// Developer utilities.
|
||||
Util(Util),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallTpLink {
|
||||
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
|
||||
/// that if an SD card is later added, your existing installation is shadowed!
|
||||
#[arg(long)]
|
||||
skip_sdcard: bool,
|
||||
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// For advanced users: Specify the path of the SD card to be mounted explicitly.
|
||||
///
|
||||
/// The default (empty string) is to use whichever sdcard path the device would use natively to
|
||||
/// mount storage on. On most TP-Link this is /media/card, but on hardware versions 9+ this is
|
||||
/// /media/sdcard
|
||||
///
|
||||
/// Only override this when the installer does not work on your hardware version, as otherwise
|
||||
/// your custom path may conflict with the builtin storage functionality.
|
||||
#[arg(long, default_value = "")]
|
||||
sdcard_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallOrbic {}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct OrbicNetworkArgs {
|
||||
/// IP address for Orbic admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Admin username for authentication.
|
||||
#[arg(long, default_value = "admin")]
|
||||
admin_username: String,
|
||||
|
||||
/// Admin password for authentication.
|
||||
#[arg(long)]
|
||||
admin_password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct InstallPinephone {}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Util {
|
||||
#[command(subcommand)]
|
||||
command: UtilSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum UtilSubCommand {
|
||||
/// Send a serial command to the Orbic.
|
||||
Serial(Serial),
|
||||
/// Start an ADB shell
|
||||
Shell,
|
||||
/// Root the Tmobile and launch adb.
|
||||
TmobileStartAdb(TmobileArgs),
|
||||
/// Root the Tmobile and launch telnetd.
|
||||
TmobileStartTelnet(TmobileArgs),
|
||||
/// Root the Uz801 and launch adb.
|
||||
Uz801StartAdb(Uz801Args),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
/// Root the Wingtech and launch telnetd.
|
||||
WingtechStartTelnet(WingtechArgs),
|
||||
/// Root the Wingtech and launch adb.
|
||||
WingtechStartAdb(WingtechArgs),
|
||||
/// Unlock the Pinephone's modem and start adb.
|
||||
PinephoneStartAdb,
|
||||
/// Lock the Pinephone's modem and stop adb.
|
||||
PinephoneStopAdb,
|
||||
/// Root the Orbic and launch telnetd.
|
||||
OrbicStartTelnet(OrbicNetworkArgs),
|
||||
/// Send a file to the TP-Link device over telnet.
|
||||
///
|
||||
/// Before running this utility, you need to make telnet accessible with `installer util
|
||||
/// tplink-start-telnet`.
|
||||
TplinkSendFile(TplinkSendFile),
|
||||
/// Send a file to the Wingtech device over telnet.
|
||||
///
|
||||
/// Before running this utility, you need to make telnet accessible with `installer util
|
||||
/// wingtech-start-telnet`.
|
||||
WingtechSendFile(WingtechSendFile),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TmobileArgs {
|
||||
/// IP address for Tmobile admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Uz801Args {
|
||||
/// IP address for Uz801 admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.100.1")]
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TplinkStartTelnet {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct TplinkSendFile {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
/// Local path to the file to send.
|
||||
local_path: String,
|
||||
/// Remote path where the file should be stored on the device.
|
||||
remote_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechSendFile {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
/// Local path to the file to send.
|
||||
local_path: String,
|
||||
/// Remote path where the file should be stored on the device.
|
||||
remote_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechArgs {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Serial {
|
||||
#[arg(long)]
|
||||
root: bool,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
|
||||
let Args { command } = Args::parse();
|
||||
|
||||
match command {
|
||||
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?,
|
||||
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
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::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => match subcommand.command {
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
if serial_cmd.root {
|
||||
if !serial_cmd.command.is_empty() {
|
||||
eprintln!("You cannot use --root and specify a command at the same time");
|
||||
std::process::exit(64);
|
||||
}
|
||||
orbic::enable_command_mode()?;
|
||||
} else if serial_cmd.command.is_empty() {
|
||||
eprintln!("Command cannot be an empty string");
|
||||
std::process::exit(64);
|
||||
} else {
|
||||
let cmd = serial_cmd.command.join(" ");
|
||||
match orbic::open_orbic()? {
|
||||
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
|
||||
None => bail!(orbic::ORBIC_NOT_FOUND),
|
||||
}
|
||||
}
|
||||
}
|
||||
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
|
||||
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
|
||||
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
UtilSubCommand::TplinkSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
||||
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
||||
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
if let Err(e) = installer::main_cli().await {
|
||||
if let Err(e) = run().await {
|
||||
eprintln!("{e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::stdin;
|
||||
|
||||
use std::io::ErrorKind;
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -12,10 +12,8 @@ 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::util::{echo, 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.
|
||||
@@ -47,21 +45,6 @@ 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;
|
||||
|
||||
@@ -71,13 +54,13 @@ const RNDIS_INTERFACE: u8 = 1;
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn confirm() -> Result<bool> {
|
||||
println!("{}", WINDOWS_WARNING);
|
||||
print!("Do you wish to proceed? Enter 'yes' to install> ");
|
||||
echo!("Do you wish to proceed? Enter 'yes' to install> ");
|
||||
let mut input = String::new();
|
||||
stdin().read_line(&mut input)?;
|
||||
Ok(input.trim() == "yes")
|
||||
}
|
||||
|
||||
pub async fn install(reset_config: bool) -> Result<()> {
|
||||
pub async fn install() -> 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."
|
||||
);
|
||||
@@ -92,13 +75,13 @@ pub async fn install(reset_config: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
let mut adb_device = force_debug_mode().await?;
|
||||
print!("Installing rootshell... ");
|
||||
echo!("Installing rootshell... ");
|
||||
setup_rootshell(&mut adb_device).await?;
|
||||
println!("done");
|
||||
print!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(adb_device, reset_config).await?;
|
||||
echo!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||
println!("done");
|
||||
print!("Testing rayhunter... ");
|
||||
echo!("Testing rayhunter... ");
|
||||
test_rayhunter(&mut adb_device).await?;
|
||||
println!("done");
|
||||
Ok(())
|
||||
@@ -106,7 +89,7 @@ pub async fn install(reset_config: bool) -> Result<()> {
|
||||
|
||||
pub async fn shell() -> Result<()> {
|
||||
println!(
|
||||
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer util orbic-shell instead, unless you want ADB access for other purposes."
|
||||
"WARNING: The orbic USB installer is likely to go away in a future version of Rayhunter. Consider using ./installer util orbic-start-telnet instead."
|
||||
);
|
||||
|
||||
println!("opening shell");
|
||||
@@ -118,11 +101,11 @@ pub async fn shell() -> Result<()> {
|
||||
async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||
println!("Forcing a switch into the debug mode to enable ADB");
|
||||
enable_command_mode()?;
|
||||
print!("ADB enabled, waiting for reboot... ");
|
||||
echo!("ADB enabled, waiting for reboot... ");
|
||||
let mut adb_device = get_adb().await?;
|
||||
adb_setup_serial(&mut adb_device).await?;
|
||||
println!("it's alive!");
|
||||
print!("Waiting for atfwd_daemon to startup... ");
|
||||
echo!("Waiting for atfwd_daemon to startup... ");
|
||||
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
|
||||
println!("done");
|
||||
Ok(adb_device)
|
||||
@@ -143,7 +126,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
|
||||
async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
|
||||
@@ -153,20 +136,14 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.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,
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"orbic\"")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
&mut adb_device,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
@@ -182,7 +159,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||
println!("done");
|
||||
print!("Waiting for reboot... ");
|
||||
echo!("Waiting for reboot... ");
|
||||
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
|
||||
// first wait for shutdown (it can take ~10s)
|
||||
tokio::time::timeout(Duration::from_secs(30), async {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
@@ -7,14 +8,9 @@ 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};
|
||||
|
||||
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
|
||||
const TELNET_PORT: u16 = 24;
|
||||
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExploitResponse {
|
||||
@@ -105,10 +101,10 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
|
||||
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Cookie", authenticated_cookie)
|
||||
// Original Orbic lacks telnetd (kajeet has it) so we need to use netcat
|
||||
.body(format!(
|
||||
r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
|
||||
))
|
||||
// Original Orbic lacks telnetd (unlike other devices)
|
||||
// When doing this, one needs to set prompt=None in the telnet utility functions
|
||||
// But some kajeet devices have password protected telnetd so we use port 24 just in case
|
||||
.body(r#"{"password": "\"; busybox nc -ll -p 24 -e /bin/sh & #"}"#)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to start telnet")?
|
||||
@@ -132,7 +128,7 @@ pub async fn start_telnet(
|
||||
anyhow::bail!("--admin-password is required");
|
||||
};
|
||||
|
||||
print!("Logging in and starting telnet... ");
|
||||
echo!("Logging in and starting telnet... ");
|
||||
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
||||
println!("done");
|
||||
|
||||
@@ -143,7 +139,6 @@ 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!(
|
||||
@@ -159,19 +154,19 @@ pub async fn install(
|
||||
anyhow::bail!("exiting");
|
||||
};
|
||||
|
||||
print!("Logging in and starting telnet... ");
|
||||
echo!("Logging in and starting telnet... ");
|
||||
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
||||
println!("done");
|
||||
|
||||
print!("Waiting for telnet to become available... ");
|
||||
echo!("Waiting for telnet to become available... ");
|
||||
wait_for_telnet(&admin_ip).await?;
|
||||
println!("done");
|
||||
|
||||
setup_rayhunter(&admin_ip, reset_config).await
|
||||
setup_rayhunter(&admin_ip).await
|
||||
}
|
||||
|
||||
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
||||
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
|
||||
let timeout = Duration::from_secs(60);
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
@@ -191,8 +186,8 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
||||
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
// Remount filesystem as read-write to allow modifications
|
||||
@@ -215,12 +210,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut conn = TelnetConnection::new(addr, false);
|
||||
install_config(
|
||||
&mut conn,
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/config.toml",
|
||||
"orbic",
|
||||
reset_config,
|
||||
CONFIG_TOML
|
||||
.replace(r#"#device = "orbic""#, r#"device = "orbic""#)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -274,16 +270,3 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Root the Orbic device and open an interactive shell
|
||||
pub async fn shell(
|
||||
admin_ip: &str,
|
||||
admin_username: &str,
|
||||
admin_password: Option<&str>,
|
||||
) -> Result<()> {
|
||||
start_telnet(admin_ip, admin_username, admin_password).await?;
|
||||
eprintln!(
|
||||
"This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands."
|
||||
);
|
||||
interactive_shell(admin_ip, TELNET_PORT, false).await
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
//! Output handling for the installer
|
||||
//!
|
||||
//! This module provides custom print macros that can be intercepted by setting
|
||||
//! a callback function. This is essential for FFI usage where stdout/stderr
|
||||
//! redirection doesn't work reliably (especially on Android).
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Type for the output callback function
|
||||
type OutputCallbackFn = Box<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
/// Global output callback storage
|
||||
static OUTPUT_CALLBACK: Mutex<Option<OutputCallbackFn>> = Mutex::new(None);
|
||||
|
||||
/// Set the global output callback
|
||||
///
|
||||
/// All output from `println!` and `eprintln!` will be sent to this callback.
|
||||
/// If no callback is set, output goes to stdout/stderr as normal.
|
||||
///
|
||||
/// Returns a guard that when dropped, resets the callback.
|
||||
pub(crate) fn set_output_callback<F>(callback: F) -> OutputCallbackGuard
|
||||
where
|
||||
F: Fn(&str) + Send + Sync + 'static,
|
||||
{
|
||||
*OUTPUT_CALLBACK.lock().unwrap() = Some(Box::new(callback));
|
||||
OutputCallbackGuard
|
||||
}
|
||||
|
||||
pub struct OutputCallbackGuard;
|
||||
|
||||
impl Drop for OutputCallbackGuard {
|
||||
fn drop(&mut self) {
|
||||
clear_output_callback();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the global output callback
|
||||
pub(crate) fn clear_output_callback() {
|
||||
*OUTPUT_CALLBACK.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
/// Write a line to the output (either callback or stdout)
|
||||
pub(crate) fn write_output_line(s: &str) {
|
||||
if let Ok(guard) = OUTPUT_CALLBACK.lock()
|
||||
&& let Some(ref callback) = *guard
|
||||
{
|
||||
callback(s);
|
||||
callback("\n");
|
||||
return;
|
||||
}
|
||||
// Fallback to stdout if no callback or lock failed
|
||||
std::println!("{}", s);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
/// Write an error line to the output (either callback or stderr)
|
||||
pub(crate) fn write_error_line(s: &str) {
|
||||
if let Ok(guard) = OUTPUT_CALLBACK.lock()
|
||||
&& let Some(ref callback) = *guard
|
||||
{
|
||||
callback(s);
|
||||
callback("\n");
|
||||
return;
|
||||
}
|
||||
// Fallback to stderr if no callback or lock failed
|
||||
std::eprintln!("{}", s);
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
|
||||
/// Write raw output without newline (either callback or stdout)
|
||||
pub(crate) fn write_output_raw(s: &str) {
|
||||
if let Ok(guard) = OUTPUT_CALLBACK.lock()
|
||||
&& let Some(ref callback) = *guard
|
||||
{
|
||||
callback(s);
|
||||
return;
|
||||
}
|
||||
// Fallback to stdout if no callback or lock failed
|
||||
std::print!("{}", s);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
/// Shadow println! macro to respect the output callback
|
||||
macro_rules! println {
|
||||
() => {
|
||||
$crate::output::write_output_line("")
|
||||
};
|
||||
($($arg:tt)*) => {{
|
||||
$crate::output::write_output_line(&format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
pub(crate) use println;
|
||||
|
||||
/// Shadow eprintln! macro to respect the output callback
|
||||
macro_rules! eprintln {
|
||||
() => {
|
||||
$crate::output::write_error_line("")
|
||||
};
|
||||
($($arg:tt)*) => {{
|
||||
$crate::output::write_error_line(&format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
pub(crate) use eprintln;
|
||||
|
||||
/// Shadow print! macro to respect the output callback
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => {{
|
||||
$crate::output::write_output_raw(&format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
pub(crate) use print;
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -9,10 +10,8 @@ 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;
|
||||
use crate::util::{echo, open_usb_device};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
const USB_VENDOR_ID: u16 = 0x2C7C;
|
||||
@@ -20,54 +19,48 @@ const USB_PRODUCT_ID: u16 = 0x125;
|
||||
const USB_INTERFACE_NUMBER: u8 = 2;
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
print!("Unlocking modem ... ");
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
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?;
|
||||
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
|
||||
adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?;
|
||||
adb.install_file(
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML
|
||||
.replace("#device = \"orbic\"", "device = \"pinephone\"")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
)?;
|
||||
adb.install_file(
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
)?;
|
||||
adb.install_file(
|
||||
"/etc/init.d/misc-daemon",
|
||||
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||
)
|
||||
.await?;
|
||||
run_command_expect(
|
||||
&mut adb,
|
||||
"chmod 755 /etc/init.d/rayhunter_daemon",
|
||||
)?;
|
||||
adb.run_command(
|
||||
&["chmod", "755", "/etc/init.d/rayhunter_daemon"],
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
run_command_expect(&mut adb, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?;
|
||||
)?;
|
||||
adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
run_command_expect(&mut adb, "shutdown -r -t 1 now", "exit code 0").await?;
|
||||
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
print!("Unlocking modem ... ");
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
print!("Testing rayhunter ... ");
|
||||
echo!("Testing rayhunter ... ");
|
||||
test_rayhunter(&mut adb).await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running on the modem. Use adb to access the web interface.");
|
||||
@@ -75,19 +68,6 @@ 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,
|
||||
@@ -195,19 +175,30 @@ pub async fn stop_adb() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
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<()> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
let cmd = ["sh", "-c", &format!("{command}; echo exit code $?")];
|
||||
let mut cmd = Vec::<&str>::new();
|
||||
cmd.extend_from_slice(command);
|
||||
cmd.extend_from_slice(&[";", "echo", "exit code $?"]);
|
||||
self.shell_command(&cmd, &mut buf)?;
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
let output = String::from_utf8_lossy(&buf);
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfer a file to the modem's filesystem with adb push.
|
||||
/// Validates the file sends successfully to /tmp before overwriting the destination.
|
||||
async fn write_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
||||
print!("Sending file {dest} ... ");
|
||||
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
||||
echo!("Sending file {dest} ... ");
|
||||
let file_name = Path::new(dest)
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
|
||||
@@ -217,16 +208,8 @@ impl DeviceConnection for ADBUSBDevice {
|
||||
let push_tmp_path = format!("/tmp/{file_name}");
|
||||
let file_hash = md5_compute(payload);
|
||||
self.push(&mut payload, &push_tmp_path)?;
|
||||
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}");
|
||||
}
|
||||
self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?;
|
||||
self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?;
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||