103 Commits

Author SHA1 Message Date
Cooper Quintin
e27da68b5d bump version to 0.10.1 2026-02-05 12:27:09 -08:00
Markus Unterwaditzer
2a68c99897 Revert "Add warning about default routes"
This reverts commit 9ae1563286.

Reverts #804
Reverts #830

Reopens #345
2026-02-05 12:21:07 -08:00
dependabot[bot]
987d95c23e Bump rsa from 0.9.8 to 0.9.10 (#853)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.8 to 0.9.10.
- [Changelog](https://github.com/RustCrypto/RSA/blob/v0.9.10/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.8...v0.9.10)

---
updated-dependencies:
- dependency-name: rsa
  dependency-version: 0.9.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 21:06:41 +01:00
dependabot[bot]
9ef6b43dac Bump time from 0.3.41 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.41 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.41...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-05 10:45:54 -08:00
Cooper Quintin
ffc42f6ffd bump version to 0.10.0 2026-02-04 13:42:38 -08:00
Markus Unterwaditzer
2781b3c7ed Remove wrong timestamp correction from diag 2026-02-04 13:36:46 -08:00
Markus Unterwaditzer
fd63210bf9 Fix prettier lint 2026-02-04 13:36:46 -08:00
Markus Unterwaditzer
a271c4ddf4 Update wording in the clock sync UI (apply suggestions from code review)
Co-authored-by: Will Greenberg <willg@eff.org>
2026-02-04 13:36:46 -08:00
Markus Unterwaditzer
bef6b51e28 Add button to set current time
When there is a significant difference between the user's browser's time
and the system time, a button appears in the web UI to fix the system
time. This time will then be used to correct both data inside of PCAPs
and any metadata.

We don't actually set the system time to this value. Instead, rayhunter
adjusts any timestamps it handles by an offset. That offset defaults to
zero, and the user adjusts it by hitting the button in the web UI. The
main reason for this is device portability.

I haven't investigated whether it would actually be easy to set the real
system time. It's possible that it works the same way across all
devices.
2026-02-04 13:36:46 -08:00
Markus Unterwaditzer
781d07230c Correct FAQ entries to refer to correct shell
The orbic now has many different shells, crosslink to the docs on shells
to find the right one.

Fix #753
Ref https://github.com/EFForg/rayhunter/discussions/842
2026-02-04 10:57:22 -08:00
Markus Unterwaditzer
1f171521e4 Filter out asn1_codecs warnings in rayhunter-daemon
...and make a small UI change so that folks won't get concerned about parsing errors.

Right now all the "undecoded extensions" noise goes into
rayhunter-daemon.log, and users get concerned about it when browsing
that through the UI.
2026-02-04 10:55:54 -08:00
Harrison Marley
5b2cf3cec4 Mention Vodafone Pocket Wifi 5 availability
Added information about 'Vodafone Pocket Wifi 5' availability in Australia.
2026-02-04 10:55:39 -08:00
Cooper Quintin
62e8d4c40f appease untitaker 2026-02-04 10:54:23 -08:00
Cooper Quintin
72c19e0f04 update docs and version 2026-02-04 10:54:23 -08:00
Cooper Quintin
9b52f46c1a appease clippy 2026-02-04 10:54:23 -08:00
Cooper Quintin
51d4e86b3a cargo fmt 2026-02-04 10:54:23 -08:00
Cooper Quintin
33fafd4707 report lack of LTE neighbors 2026-02-04 10:54:23 -08:00
Cooper Quintin
6e4cbac4b1 convert int to option 2026-02-04 10:54:23 -08:00
Cooper Quintin
b453c92d6a Update lib/src/analysis/priority_2g_downgrade.rs
Co-authored-by: Will Greenberg <willg@eff.org>
2026-02-04 10:54:23 -08:00
Cooper Quintin
733c8b227d Update lib/src/analysis/analyzer.rs
Co-authored-by: Will Greenberg <willg@eff.org>
2026-02-04 10:54:23 -08:00
Markus Unterwaditzer
b43217ef35 Fix compilation errors 2026-02-04 10:54:23 -08:00
Cooper Quintin
40a0dec361 appease clippy 2026-02-04 10:54:23 -08:00
Cooper Quintin
b2d5ed356f cargo fmt 2026-02-04 10:54:23 -08:00
Cooper Quintin
6033757ddb compare gsm and 3g priorities to LTE priorities and only alert if higher priority or only non LTE neighbors exist 2026-02-04 10:54:23 -08:00
dependabot[bot]
6b4f98183e Bump bytes from 1.10.1 to 1.11.1 (#847)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 20:38:53 +01:00
kmille
bd2329d6cc Show tracking area code for test heuristic in UI (#843) 2026-02-03 10:59:15 +01:00
Michael S.
d1311e0ba3 Docs: Add note about non-default credentials to install instructions and modify link target (#839) 2026-02-02 11:44:20 +01:00
Markus Unterwaditzer
75cf03d638 Fix rayhunter chugging on when diag device failed to initialize
Ref #676 -- this is a partial fix for one of the issues mentioned there.

I expect that as a result we'll get more bugreports about rayhunter not
starting, since right now those errors are "masked" by this bug.
2026-02-01 20:32:40 +01:00
Markus Unterwaditzer
be15035ad4 Use /proc/net/route instead of ip route command
Fix #811, allegedly /proc/net/route is almost always available, and no
additional dependency is needed at all.
2026-01-30 12:16:32 -08:00
Markus Unterwaditzer
a3d0d8f4f9 Better support for firmware-devel profile
Currently you have to override a bunch of paths to use firmware-devel
when building the installer. This changes that, and adds a new
FIRMWARE_PROFILE envvar that can be used to fix both rootshell and
rayhunter-daemon paths at the same time.

There is now also a new cargo command for building rootshell, similar to
how building the daemon firmware works.

I'm not sure what to do with make.sh. I have personally never used it.
2026-01-30 21:09:12 +01:00
Markus Unterwaditzer
2c30218743 Add donation button to UI 2026-01-30 10:14:57 -08:00
Markus Unterwaditzer
eb65214989 Add FAQ to issue templates 2026-01-30 10:14:57 -08:00
Markus Unterwaditzer
8d86aeb591 Restructure docs, make FAQ more prominent 2026-01-30 10:14:57 -08:00
Markus Unterwaditzer
23cef7349e Upgrade num-bigint-dig
This dependency is causing warnings during compilation, vieable with `cargo report future-incompatibilities --id 1`
2026-01-29 20:03:22 +01:00
Markus Unterwaditzer
07e0115192 Add disclaimer about flickering 2026-01-29 20:02:09 +01:00
Markus Unterwaditzer
82b53c6187 Display: Implement High Vis Mode
Work around #722 (we should probably still fix it properly)
Fix #779
2026-01-29 20:02:09 +01:00
Markus Unterwaditzer
883175aa59 Disable blank issues again
There are too many questions in the issue tracker recently.
2026-01-29 11:01:46 -08:00
Brad Warren
bd52718ea7 add {daemon,rootshell}_needed GHA outputs 2026-01-28 15:31:27 -08:00
Markus Unterwaditzer
d607c63cc8 Do not overwrite configs by default
On tplink and orbic, do not overwrite config files by default. There is
a new flag `installer orbic --reset-config` that one can use to restore
the old behavior. This fixes #778, a long-standing issue existent since
0.3.0.

The businesslogic for config file overrides is shared to some degree.
The Install trait from pinephone.rs has been moved out and renamed to
DeviceConnection for that purpose, so that `install_config` can be
shared across installers, which in turn can delegate to the trait for
running commands and copying files. This also works towards #542.

However, the pinephone and other installers have not been adapted to
support --reset-config out of fear of regressions. A future refactor by
somebody with ability to test on pinephone should probably also consider
using the same DeviceConnection impl as orbic, if possible.
2026-01-28 10:35:57 -08:00
Will Greenberg
9e08e662ff installer-gui: add an npm prepare step
seems this is required due to
https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md#minor-changes-34
2026-01-27 11:54:36 -08:00
dependabot[bot]
08920e02b8 Bump @sveltejs/kit from 2.48.3 to 2.50.1 in /installer-gui
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.48.3 to 2.50.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.50.1/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.50.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 11:54:36 -08:00
Markus Unterwaditzer
7e2df91702 Fix battery warnings on unsupported devices
Fix #644, break early if battery is unsupported.
2026-01-26 11:22:47 -08:00
Markus Unterwaditzer
262f583355 Add price information to the orbic page
Since rayhunter is getting more popular let's set some expectations
around pricing. There are currently offers ranging from 20 to 90 USD,
and people should understand that the 20 USD offer is not a fake, or
anything like that.
2026-01-26 11:13:21 -08:00
Markus Unterwaditzer
9ae1563286 Add warning about default routes
I hope this puts a lot of questions about SIM cards to rest. I found
that the warning also sometimes applies to "dead" SIM cards which have
expired a long time ago.

Run `busybox ip route` to determine whether the device has an active SIM
card. That command has been manually tested on Moxee, Orbic and TP-Link.
It's prefixed with `busybox` because that makes it more likely it would
work on UZ801, though it wasn't tested there. If the command invocation
fails, the alert is suppressed and a warning is logged.

The command is only run once on pageload. It could've been part of the
status endpoint, but then the UI would poll it way too often.
2026-01-26 11:08:38 -08:00
Markus Unterwaditzer
2bd6efa503 UI: Enforce snake case for functions
It's more common to write functions in camelCase in JS, so some people
started doing it, including me. But the majority of the codebase is
snake_case, so let's enforce that.
2026-01-26 10:35:22 -08:00
Markus Unterwaditzer
e06769158b add #build-all functionality to CI 2026-01-26 10:33:56 -08:00
Markus Unterwaditzer
b341ef2d1e Fix stuck green display after restarting recording
Rayhunter keeps track of the highest-severity warning seen during a
recording, and only updates the display color when a new event
exceeds that level. When a double-tap restarts recording, this
threshold isn't reset, so it retains the old session's maximum. Since no
new event can surpass the stale threshold, the display stays stuck on
green even when warnings are detected.

Fix #794
2026-01-26 10:33:56 -08:00
dependabot[bot]
3a807f48b2 Bump @sveltejs/kit from 2.46.2 to 2.49.5 in /daemon/web
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.46.2 to 2.49.5.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.49.5/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.49.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-15 23:55:28 +01:00
dependabot[bot]
bc3f0bf515 Bump devalue from 5.4.2 to 5.6.2 in /installer-gui
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.4.2 to 5.6.2.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.4.2...v5.6.2)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-15 23:55:13 +01:00
Rupert Carr
d3290a2c2d Add test notification endpoint and UI button
- Add POST /api/test-notification endpoint to send test to saved config URL
- Refactor send_notification to return Result instead of bool
- Add NotificationError enum for proper error handling
- Add test notification button in config UI with explanatory text
- Button tests saved configuration URL, not input field value
2026-01-07 14:18:18 +01:00
Rupert Carr
579c2c1f3f Add e2e tests for notification worker and extract send_notification helper 2026-01-07 14:18:18 +01:00
Cooper Quintin
7977a01a88 version bump 2026-01-06 09:45:58 -08:00
Cooper Quintin
78dd2f74a4 version bump 2026-01-06 09:28:40 -08:00
Markus Unterwaditzer
dd70a2a15d Add mount logs to rayhunter installer
We sometimes, but rarely, get bug reports where the sdcard fails
mounting. Write a dedicated log file for the mounting action to /tmp,
separately from the rayhunter logfile that is on the sdcard itself. That
log file is probably going to be small so it can fit in /tmp.
2026-01-06 17:42:40 +01:00
Markus Unterwaditzer
81a193959c fix another diff in behavior 2026-01-06 17:42:26 +01:00
Markus Unterwaditzer
7209910c11 Fix deku 0.20 discriminant double-read in Nas4GMessage
Applied workaround from sharksforarms/deku#305 using:
  #[deku(skip, default = "log_type")]

Found using differential fuzzing.

This may be a bug in deku.
2026-01-06 17:42:26 +01:00
Markus Unterwaditzer
3615cbf2dd Upgrade deku to 0.20
Fix #748
2026-01-05 14:32:32 -08:00
Markus Unterwaditzer
61793179e5 Fix Message parser crashes found by fuzzing
These payloads would previous cause panic on underflow.

The fuzzing setup lives in
https://github.com/untitaker/rayhunter/tree/fuzz-wip -- I can eventually
upstream it though right now it runs very inefficiently.
2025-12-09 21:31:08 +01:00
Vicente Reyes
cdc7a46162 Small grammar change 2025-12-03 09:56:25 -08:00
Markus Unterwaditzer
ffe58ab72b Remove powershell script (#715)
* Remove powershell script

Currently install.ps1 and installer are both released in the root of the
zipfile. I think that's a bit confusing. We also don't really support
the ps1 script since a while.

* Remove rootshell and config.toml.in from release folder
2025-12-03 12:08:51 +01:00
Markus Unterwaditzer
7906bf7d67 use cfmakeraw 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
5e4174c9f3 address review feedback 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
2a8fee25f9 Remove mentions of tplink-start-telnet and orbic-start-telnet 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
516e878661 fix installation instructions for orbic 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
5fbc540fa0 Implement basic telnet shell for both orbic and tplink 2025-11-25 13:52:07 -08:00
Brad Warren
676cd3c862 update installer-gui version to 0.8.0 2025-11-24 11:56:26 -08:00
Brad Warren
a8cb363112 run zizmor --fix=all . 2025-11-24 11:54:01 -08:00
dependabot[bot]
6172236a3c Bump glob from 10.4.5 to 10.5.0 in /daemon/web
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 21:31:21 +01:00
Markus Unterwaditzer
485d1a99f6 Revert back to the CLI using Clap more directly 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
f6e118a5cc convert arg parsing errors into stderr printing, remove main_cli 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
4cdc9961d3 fix argv0 bug and update lockfile 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
c18579583c remove shell:default permission 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
565b6d188d remove unused gen folder 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
80f12ffaaa fix github actions for windows/mac 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
3e9af006e1 remove tauri-shell entirely 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
73a5d324c4 clean up run_with_callback api 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
bb6135c682 Apply suggestion from @oopsbagel
Co-authored-by: oopsbagel <99793478+oopsbagel@users.noreply.github.com>
2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
3b44234ae1 implement installer as library and use it in gui 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
9e9fe4d392 write new main.rs 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
2c92315125 rename installer main.rs to lib.rs 2025-11-18 21:05:22 +01:00
dependabot[bot]
7bc55bf432 Bump js-yaml from 4.1.0 to 4.1.1 in /daemon/web (#705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 15:41:31 +01:00
Will Greenberg
2a7c5b4365 Add logo SVGs
Fixes #680
2025-11-17 12:09:25 -08:00
dependabot[bot]
d48d5755c6 Bump js-yaml from 4.1.0 to 4.1.1 in /installer-gui (#702)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-16 15:31:24 +01:00
Cooper Quintin
1cf1d6d5b9 fix 642 2025-11-12 11:37:27 -08:00
Dylan Buel
c8d1b52ca7 Removed reference to deleted documentation and added language about updating to landing page (#697)
* Removed references to installing-from-release-windows.md removed in commit ea5aa6cee2

* Added language referencing the upgrade instructions in installation landing page

* Update doc/installation.md

---------

Co-authored-by: Markus Unterwaditzer <markus-github@unterwaditzer.net>
2025-11-09 12:36:36 +01:00
Markus Unterwaditzer
04efe7bb75 One pass of cargo-audit
Upgrade some yanked dependencies to non-yanked (windows-core) and ignore
the other two warnings.
2025-11-06 17:01:41 +01:00
Brad Warren
3f3b6168b3 remove license 2025-11-05 10:53:41 -08:00
Brad Warren
992a28af57 add README 2025-11-05 10:53:41 -08:00
Brad Warren
39c8844967 update ci config 2025-11-05 10:53:41 -08:00
Brad Warren
ef006d83a6 write plumbing to & from CLI installer 2025-11-05 10:53:41 -08:00
Brad Warren
bc9022530a cargo add anyhow --package installer-gui 2025-11-05 10:53:41 -08:00
Brad Warren
af2445cc38 remove frontend boilerplate 2025-11-05 10:53:41 -08:00
Brad Warren
e33f143830 add rayhunter banner 2025-11-05 10:53:41 -08:00
Brad Warren
f5360b042c set up tailwindcss 2025-11-05 10:53:41 -08:00
Brad Warren
a16fb9b678 set up eslint 2025-11-05 10:53:41 -08:00
Brad Warren
3349895a3e set up prettier 2025-11-05 10:53:41 -08:00
Brad Warren
30b517069a bundle cli-installer 2025-11-05 10:53:41 -08:00
Brad Warren
4efc2d5db3 npm run tauri add shell 2025-11-05 10:53:41 -08:00
Brad Warren
5e066682b3 run npm run tauri icon & exclude mobile icons 2025-11-05 10:53:41 -08:00
Brad Warren
01aefe25c9 update Cargo.toml and run npm run tauri dev 2025-11-05 10:53:41 -08:00
Brad Warren
e8e9f9366c clean up tauri boilerplate 2025-11-05 10:53:41 -08:00
Brad Warren
fa346989e6 run npm install
we need to track package-lock.json to keep the tauri JS packages in sync
with tauri's rust packages in cargo.lock
2025-11-05 10:53:41 -08:00
Brad Warren
d942545ac3 run create-tauri-app
command was: sh <(curl https://create.tauri.app/sh)

the chosen options were:

Project name · installer-gui
Identifier · com.rayhunter-installer.app
Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
Choose your package manager · npm
Choose your UI template · Svelte - (https://svelte.dev/)
Choose your UI flavor · TypeScript
2025-11-05 10:53:41 -08:00
114 changed files with 10256 additions and 3050 deletions

11
.cargo/audit.toml Normal file
View File

@@ -0,0 +1,11 @@
[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",
]

View File

@@ -5,6 +5,10 @@ build-daemon-firmware = "build -p rayhunter-daemon --bin rayhunter-daemon --targ
# Build the daemon with "firmware-devel" profile and "rustcrypto" backend.
# Works with just the Rust toolchain, and is medium-slow to build. Binaries are slightly larger.
build-daemon-firmware-devel = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware-devel"
# Build rootshell for firmware
build-rootshell-firmware = "build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware"
# Build rootshell for development
build-rootshell-firmware-devel = "build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware-devel"
[target.aarch64-apple-darwin]
linker = "rust-lld"

View File

@@ -1,5 +1,7 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://efforg.github.io/rayhunter/faq.html
- name: Questions and community
url: https://efforg.github.io/rayhunter/support-feedback-community.html
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help on GitHub discussions or Mattermost

View File

@@ -1,7 +1,8 @@
## Pull Request Checklist
- [ ] The Rayhunter team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [ ] The Rayhunter team has recently expressed interest in reviewing a PR for this.
- If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [ ] Added or updated any documentation as needed to support the changes in this PR.
- [ ] Code has been linted and run through `cargo fmt`
- [ ] If any new functionality has been added, unit tests were also added
- [ ] [./CONTRIBUTING.md](../CONTRIBUTING.md) has been read
- [ ] Code has been linted and run through `cargo fmt`.
- [ ] If any new functionality has been added, unit tests were also added.
- [ ] [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md) has been read.

View File

@@ -20,49 +20,70 @@ jobs:
permissions:
contents: read
outputs:
code_changed: ${{ steps.files_changed.outputs.code_count }}
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
web_changed: ${{ steps.files_changed.outputs.web_count }}
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
code_changed: ${{ steps.files_changed.outputs.code_count != '0' }}
daemon_changed: ${{ steps.files_changed.outputs.daemon_count != '0' }}
daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
web_changed: ${{ steps.files_changed.outputs.web_count != '0' }}
docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' }}
installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }}
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }}
rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
steps:
- uses: actions/checkout@v4
with:
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, or if these workflow files are being changed, run everything
if [ ${{ github.ref }} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo
# If we are on main, if workflow/cargo config files changed, or if
# the latest commit message contains "#build-all", run everything.
# Use #build-all in a commit message to force a full build on a PR
# branch (useful for testing release builds without merging to main).
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo || git log -1 --format='%s %b' | grep -qF '#build-all'
then
echo "building everything"
echo code_count=forced >> "$GITHUB_OUTPUT"
echo daemon_count=forced >> "$GITHUB_OUTPUT"
echo web_count=forced >> "$GITHUB_OUTPUT"
echo docs_count=forced >> "$GITHUB_OUTPUT"
echo installer_build=forced >> "$GITHUB_OUTPUT"
echo installer_count=forced >> "$GITHUB_OUTPUT"
echo installer_gui_count=forced >> "$GITHUB_OUTPUT"
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
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 != '0'
if: needs.files_changed.outputs.docs_changed == 'true'
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
@@ -80,6 +101,8 @@ 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
@@ -98,12 +121,14 @@ jobs:
check_and_test:
needs: files_changed
if: needs.files_changed.outputs.code_changed != '0'
if: needs.files_changed.outputs.code_changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
@@ -124,9 +149,37 @@ jobs:
run: |
NO_FIRMWARE_BIN=true cargo clippy --verbose
test_web_frontend:
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
needs: files_changed
if: needs.files_changed.outputs.web_changed != '0'
if: needs.files_changed.outputs.installer_gui_changed == 'true'
# we run this on macos simply because no additional OS packages need to be
# installed
runs-on: macos-latest
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'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -135,19 +188,40 @@ 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 != '0'
if: needs.files_changed.outputs.installer_changed == 'true'
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
@@ -161,7 +235,7 @@ jobs:
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
build_rayhunter_check:
if: needs.files_changed.outputs.daemon_changed != '0'
if: needs.files_changed.outputs.daemon_changed == 'true'
needs:
- check_and_test
- files_changed
@@ -192,6 +266,8 @@ 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 }}
@@ -205,7 +281,7 @@ jobs:
if-no-files-found: error
build_rootshell:
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
if: needs.files_changed.outputs.rootshell_needed == 'true'
needs:
- check_and_test
- files_changed
@@ -214,6 +290,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
@@ -227,10 +305,7 @@ jobs:
if-no-files-found: error
build_rayhunter:
# build_rust_installer needs this step. so when installer_changed, we need
# to build this step too. if we skip this step because only the installer
# changed, the build_rust_installer step will be skipped too.
if: needs.files_changed.outputs.daemon_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
if: needs.files_changed.outputs.daemon_needed == 'true'
needs:
- check_and_test
- files_changed
@@ -240,6 +315,8 @@ 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
@@ -268,7 +345,7 @@ jobs:
if-no-files-found: error
build_rust_installer:
if: needs.files_changed.outputs.installer_changed != '0'
if: needs.files_changed.outputs.installer_changed == 'true'
permissions:
contents: read
packages: write
@@ -301,6 +378,8 @@ 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:
@@ -313,6 +392,145 @@ 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
@@ -334,6 +552,8 @@ 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
@@ -343,7 +563,7 @@ jobs:
- name: Setup versioned release directory
run: |
platform="${{ matrix.platform }}"
dest="rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}"
dest="rayhunter-v${VERSION}-${{ matrix.platform }}"
mkdir "$dest"
# Handle installer with proper extension for Windows
if [ "$platform" = "windows-x86_64" ]; then
@@ -351,7 +571,7 @@ jobs:
else
mv installer-$platform/installer "$dest"/installer
fi
cp -r rayhunter-check-* rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
cp -r rayhunter-check-* rayhunter-daemon dist/scripts "$dest"/
zip -r "$dest.zip" "$dest"
sha256sum "$dest.zip" > "$dest.zip.sha256"

View File

@@ -14,10 +14,12 @@ 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 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 {} \;
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 {} \;
echo number of defined versions = $defined_versions
if [ $defined_versions != "1" ]
then
@@ -41,6 +43,8 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- name: Create release
run: |

3542
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,5 +7,17 @@ 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"

View File

@@ -3,7 +3,7 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
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).
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).
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.
&rarr; Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-check"
version = "0.8.0"
version = "0.10.1"
edition = "2024"
[dependencies]
@@ -10,5 +10,4 @@ log = "0.4.20"
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
pcap-file-tokio = "0.1.0"
clap = { version = "4.5.2", features = ["derive"] }
simple_logger = "5.0.0"
walkdir = "2.5.0"

View File

@@ -177,14 +177,7 @@ async fn main() {
} else {
log::LevelFilter::Info
};
simple_logger::SimpleLogger::new()
.with_colors(true)
.without_timestamps()
.with_level(level)
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
.with_module_level("asn1_codecs", log::LevelFilter::Error)
.init()
.unwrap();
rayhunter::init_logging(level);
let harness = Harness::new_with_config(&AnalyzerConfig::default());
info!("Analyzers:");

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.8.0"
version = "0.10.1"
edition = "2024"
rust-version = "1.88.0"
@@ -18,7 +18,6 @@ axum = { version = "0.8", default-features = false, features = ["http1", "tokio"
thiserror = "1.0.52"
libc = "0.2.150"
log = "0.4.20"
env_logger = { version = "0.11", default-features = false }
tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
futures-macro = "0.3.30"
include_dir = "0.7.3"
@@ -33,4 +32,3 @@ 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"] }

View File

@@ -1,6 +1,6 @@
use std::{path::Path, time::Duration};
use log::{error, info};
use log::{info, warn};
use rayhunter::Device;
use serde::Serialize;
use tokio::select;
@@ -66,11 +66,11 @@ pub fn run_battery_notification_worker(
// Don't send a notification initially if the device starts at a low battery level.
let mut triggered = match get_battery_status(&device).await {
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
info!("Battery level function not supported for device");
false
info!("Battery status not supported for this device, disabling battery notifications");
return;
}
Err(e) => {
error!("Failed to get battery status: {e}");
warn!("Failed to get battery status: {e}");
true
}
Ok(status) => status.level <= LOW_BATTERY_LEVEL,
@@ -83,8 +83,12 @@ pub fn run_battery_notification_worker(
}
let status = match get_battery_status(&device).await {
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
info!("Battery status not supported for this device, disabling battery notifications");
break;
}
Err(e) => {
error!("Failed to get battery status: {e}");
warn!("Failed to get battery status: {e}");
continue;
}
Ok(status) => status,

View File

@@ -77,6 +77,7 @@ impl DiagTask {
/// Start recording
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
self.max_type_seen = EventType::Informational;
let (qmdl_file, analysis_file) = qmdl_store
.new_entry()
.await

View File

@@ -178,6 +178,7 @@ 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;
@@ -214,9 +215,13 @@ pub fn update_ui(
Err(e) => error!("error receiving framebuffer update message: {e}"),
}
let mut status_bar_height = 2;
match display_level {
2 => fb.draw_gif(img.unwrap()).await,
3 => fb.draw_img(img.unwrap()).await,
4 => {
status_bar_height = fb.dimensions().height;
}
128 => {
fb.draw_line(Color::Cyan, 128).await;
fb.draw_line(Color::Pink, 102).await;
@@ -224,12 +229,13 @@ pub fn update_ui(
fb.draw_line(Color::Pink, 50).await;
fb.draw_line(Color::Cyan, 25).await;
}
// this branch id for ui_level 1, which is also the default if an
// this branch is for ui_level 1, which is also the default if an
// unknown value is used
_ => {}
};
let (color, pattern) = display_style;
fb.draw_patterned_line(color, 2, pattern).await;
fb.draw_patterned_line(color, status_bar_height, pattern)
.await;
tokio::time::sleep(Duration::from_millis(REFRESH_RATE)).await;
}
});

View File

@@ -22,7 +22,8 @@ use crate::notifications::{NotificationService, run_notification_worker};
use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore;
use crate::server::{
ServerState, debug_set_display_state, get_config, get_qmdl, get_zip, serve_static, set_config,
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_zip, serve_static,
set_config, set_time_offset, test_notification,
};
use crate::stats::{get_qmdl_manifest, get_system_stats};
@@ -68,6 +69,9 @@ fn get_router() -> AppRouter {
.route("/api/analysis/{name}", post(start_analysis))
.route("/api/config", get(get_config))
.route("/api/config", post(set_config))
.route("/api/test-notification", post(test_notification))
.route("/api/time", get(get_time))
.route("/api/time-offset", post(set_time_offset))
.route("/api/debug/display-state", post(debug_set_display_state))
.route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/{*path}", get(serve_static))
@@ -167,7 +171,7 @@ fn run_shutdown_thread(
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), RayhunterError> {
env_logger::init();
rayhunter::init_logging(log::LevelFilter::Info);
#[cfg(feature = "rustcrypto-tls")]
{
@@ -203,6 +207,10 @@ async fn run_with_config(
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let restart_token = CancellationToken::new();
let shutdown_token = restart_token.child_token();
// Ensure shutdown_token is cancelled when this function exits for any
// reason (e.g. diag device init failure), so all spawned tasks get
// signaled to stop.
let _shutdown_guard = shutdown_token.clone().drop_guard();
let notification_service = NotificationService::new(config.ntfy_url.clone());

View File

@@ -6,9 +6,18 @@ use std::{
use log::error;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::mpsc::{self, error::TryRecvError};
use tokio_util::task::TaskTracker;
#[derive(Error, Debug)]
pub enum NotificationError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("Server returned error status: {0}")]
HttpError(reqwest::StatusCode),
}
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum NotificationType {
Warning,
@@ -60,6 +69,21 @@ impl NotificationService {
}
}
/// Sends a notification message to the specified URL.
pub async fn send_notification(
http_client: &reqwest::Client,
url: &str,
message: String,
) -> Result<(), NotificationError> {
let response = http_client.post(url).body(message).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(NotificationError::HttpError(response.status()))
}
}
pub fn run_notification_worker(
task_tracker: &TaskTracker,
mut notification_service: NotificationService,
@@ -125,24 +149,15 @@ pub fn run_notification_worker(
}
}
match http_client
.post(&url)
.body(notification.message.clone())
.send()
.await
match send_notification(&http_client, &url, notification.message.clone()).await
{
Ok(response) => {
if response.status().is_success() {
notification.last_sent = Some(Instant::now());
notification.failed_since_last_success = 0;
notification.needs_sending = false;
} else {
notification.failed_since_last_success += 1;
notification.last_attempt = Some(Instant::now());
}
Ok(()) => {
notification.last_sent = Some(Instant::now());
notification.failed_since_last_success = 0;
notification.needs_sending = false;
}
Err(e) => {
error!("Failed to send notification to ntfy: {e}");
error!("Failed to send notification: {e}");
notification.failed_since_last_success += 1;
notification.last_attempt = Some(Instant::now());
}
@@ -162,3 +177,205 @@ pub fn run_notification_worker(
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Bytes, extract::State, routing::post};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TestServerState {
received_messages: Arc<Mutex<Vec<String>>>,
}
async fn capture_notification(
State(state): State<TestServerState>,
body: Bytes,
) -> &'static str {
let message = String::from_utf8_lossy(&body).to_string();
state.received_messages.lock().await.push(message);
"OK"
}
async fn setup_test_server() -> (Arc<Mutex<Vec<String>>>, String) {
#[cfg(feature = "rustcrypto-tls")]
{
let _ = rustls_rustcrypto::provider().install_default();
}
let received_messages = Arc::new(Mutex::new(Vec::new()));
let test_state = TestServerState {
received_messages: received_messages.clone(),
};
let app = Router::new()
.route("/", post(capture_notification))
.with_state(test_state);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let url = format!("http://{}", addr);
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(100)).await;
(received_messages, url)
}
async fn cleanup_worker(sender: mpsc::Sender<Notification>, tracker: TaskTracker) {
drop(sender);
tracker.close();
tracker.wait().await;
}
#[tokio::test]
async fn test_notification_worker_sends_message() {
let (received_messages, url) = setup_test_server().await;
let task_tracker = TaskTracker::new();
let notification_service = NotificationService::new(Some(url));
let notification_sender = notification_service.new_handler();
run_notification_worker(
&task_tracker,
notification_service,
vec![NotificationType::Warning],
);
notification_sender
.send(Notification::new(
NotificationType::Warning,
"test warning message".to_string(),
None,
))
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
let messages = received_messages.lock().await;
assert_eq!(messages.len(), 1);
assert_eq!(messages[0], "test warning message");
drop(messages);
cleanup_worker(notification_sender, task_tracker).await;
}
#[tokio::test]
async fn test_notification_worker_filters_disabled_types() {
let (received_messages, url) = setup_test_server().await;
let task_tracker = TaskTracker::new();
let notification_service = NotificationService::new(Some(url));
let notification_sender = notification_service.new_handler();
run_notification_worker(
&task_tracker,
notification_service,
vec![NotificationType::Warning],
);
notification_sender
.send(Notification::new(
NotificationType::Warning,
"test warning".to_string(),
None,
))
.await
.unwrap();
notification_sender
.send(Notification::new(
NotificationType::LowBattery,
"test low battery".to_string(),
None,
))
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
let messages = received_messages.lock().await;
assert_eq!(messages.len(), 1);
assert_eq!(messages[0], "test warning");
drop(messages);
cleanup_worker(notification_sender, task_tracker).await;
}
#[tokio::test]
async fn test_notification_worker_sends_enabled_types() {
let (received_messages, url) = setup_test_server().await;
let task_tracker = TaskTracker::new();
let notification_service = NotificationService::new(Some(url));
let notification_sender = notification_service.new_handler();
run_notification_worker(
&task_tracker,
notification_service,
vec![NotificationType::Warning, NotificationType::LowBattery],
);
notification_sender
.send(Notification::new(
NotificationType::Warning,
"test warning".to_string(),
None,
))
.await
.unwrap();
notification_sender
.send(Notification::new(
NotificationType::LowBattery,
"test low battery".to_string(),
None,
))
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
let messages = received_messages.lock().await;
assert_eq!(messages.len(), 2);
// these are interchangeable, ordering not guaranteed
assert!(messages.contains(&"test warning".to_string()));
assert!(messages.contains(&"test low battery".to_string()));
drop(messages);
cleanup_worker(notification_sender, task_tracker).await;
}
#[tokio::test]
async fn test_notification_worker_with_no_url() {
let task_tracker = TaskTracker::new();
let notification_service = NotificationService::new(None);
let notification_sender = notification_service.new_handler();
run_notification_worker(
&task_tracker,
notification_service,
vec![NotificationType::Warning],
);
notification_sender
.send(Notification::new(
NotificationType::Warning,
"test warning".to_string(),
None,
))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
cleanup_worker(notification_sender, task_tracker).await;
}
}

View File

@@ -58,7 +58,7 @@ pub struct ManifestEntry {
impl ManifestEntry {
fn new() -> Self {
let now = Local::now();
let now = rayhunter::clock::get_adjusted_now();
let metadata = RuntimeMetadata::new();
ManifestEntry {
name: format!("{}", now.timestamp()),
@@ -300,7 +300,8 @@ impl RecordingStore {
size_bytes: usize,
) -> Result<(), RecordingStoreError> {
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
self.manifest.entries[entry_index].last_message_time =
Some(rayhunter::clock::get_adjusted_now());
self.write_manifest().await
}

View File

@@ -9,7 +9,9 @@ use axum::extract::State;
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use chrono::{DateTime, Local};
use log::{error, warn};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::fs::write;
use tokio::io::{AsyncReadExt, copy, duplex};
@@ -136,6 +138,76 @@ pub async fn set_config(
))
}
pub async fn test_notification(
State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> {
let url = state.config.ntfy_url.as_ref().ok_or((
StatusCode::BAD_REQUEST,
"No notification URL configured".to_string(),
))?;
if url.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
"Notification URL is empty".to_string(),
));
}
let http_client = reqwest::Client::new();
let message = "Test notification from Rayhunter".to_string();
crate::notifications::send_notification(&http_client, url, message)
.await
.map(|()| {
(
StatusCode::OK,
"Test notification sent successfully".to_string(),
)
})
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to send test notification: {e}"),
)
})
}
/// Response for GET /api/time
#[derive(Serialize)]
pub struct TimeResponse {
/// The raw system time (without clock offset)
pub system_time: DateTime<Local>,
/// The adjusted time (system time + offset)
pub adjusted_time: DateTime<Local>,
/// The current offset in seconds
pub offset_seconds: i64,
}
/// Request for POST /api/time-offset
#[derive(Deserialize)]
pub struct SetTimeOffsetRequest {
/// The offset to set, in seconds
pub offset_seconds: i64,
}
pub async fn get_time() -> Json<TimeResponse> {
let system_time = Local::now();
let adjusted_time = rayhunter::clock::get_adjusted_now();
let offset_seconds = adjusted_time
.signed_duration_since(system_time)
.num_seconds();
Json(TimeResponse {
system_time,
adjusted_time,
offset_seconds,
})
}
pub async fn set_time_offset(Json(req): Json<SetTimeOffsetRequest>) -> StatusCode {
rayhunter::clock::set_offset(chrono::TimeDelta::seconds(req.offset_seconds));
StatusCode::OK
}
pub async fn get_zip(
State(state): State<Arc<ServerState>>,
Path(entry_name): Path<String>,

View File

@@ -37,6 +37,17 @@ export default ts.config(
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'function',
format: ['snake_case'],
},
{
selector: 'method',
format: ['snake_case'],
},
],
},
}
);

View File

@@ -10,7 +10,7 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.13.0",
"@sveltejs/kit": "^2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.0",
"@types/node": "^24.7.0",
@@ -1167,9 +1167,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.46.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.46.2.tgz",
"integrity": "sha512-bGs473Gj4TwFf7dw6ZUwQI0ayaDb83E7G06QnYeNQC2DmAaktgFU2uB0tSfZVhpHqYH4o8GsLBkG3ZjThtmsIA==",
"version": "2.49.5",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.5.tgz",
"integrity": "sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -1179,7 +1179,7 @@
"@types/cookie": "^0.6.0",
"acorn": "^8.14.1",
"cookie": "^0.6.0",
"devalue": "^5.3.2",
"devalue": "^5.6.2",
"esm-env": "^1.2.2",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
@@ -1198,11 +1198,15 @@
"@opentelemetry/api": "^1.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": "^5.3.3",
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
@@ -2197,9 +2201,9 @@
}
},
"node_modules/devalue": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==",
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
"dev": true,
"license": "MIT"
},
@@ -2767,9 +2771,9 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3041,9 +3045,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -17,7 +17,7 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.13.0",
"@sveltejs/kit": "^2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.0",
"@types/node": "^24.7.0",

View File

@@ -78,7 +78,8 @@
<p class="text-lg underline">Unparsed Messages</p>
<p>
These are due to a limitation or bug in Rayhunter's parser, and aren't usually a
problem.
problem. We'll not accept bug reports about them unless something else is going wrong
(such as false-positives or definite false-negatives)
</p>
<div class="overflow-x-auto">
<table class="table-auto text-left">

View File

@@ -12,6 +12,7 @@
onclick,
ariaLabel,
errorMessage,
jsonBody,
}: {
url: string;
method?: string;
@@ -23,6 +24,7 @@
onclick?: () => void | Promise<void>;
ariaLabel?: string;
errorMessage?: string;
jsonBody?: unknown;
} = $props();
let is_requesting = $state(false);
@@ -43,7 +45,7 @@
},
};
async function handleClick() {
async function handle_click() {
if (is_disabled) return;
is_requesting = true;
@@ -51,7 +53,8 @@
await user_action_req(
method,
url,
errorMessage ? errorMessage : 'Error performing action'
errorMessage ? errorMessage : 'Error performing action',
jsonBody
);
if (onclick) {
await onclick();
@@ -71,7 +74,7 @@
<button
class="text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row items-center gap-1 {buttonClasses}"
onclick={handleClick}
onclick={handle_click}
disabled={is_disabled}
aria-label={ariaLabel || label}
>

View File

@@ -0,0 +1,121 @@
<script lang="ts">
import { get_daemon_time } from '$lib/utils.svelte';
import ApiRequestButton from './ApiRequestButton.svelte';
let show_alert = $state(false);
let device_system_time = $state('');
let device_adjusted_time = $state('');
let browser_time = $state('');
let has_offset = $state(false);
let computed_offset = $state(0);
let dismissed = $state(false);
let check_completed = $state(false);
const DRIFT_THRESHOLD_SECONDS = 30;
function format_time(date: Date): string {
return date.toLocaleString();
}
async function check_clock_drift() {
if (check_completed) return;
try {
const daemon_time_response = await get_daemon_time();
const browser_now = new Date();
const daemon_system_ms = new Date(daemon_time_response.system_time).getTime();
const device_adjusted_ms = new Date(daemon_time_response.adjusted_time).getTime();
const drift_seconds = Math.round((browser_now.getTime() - device_adjusted_ms) / 1000);
if (Math.abs(drift_seconds) > DRIFT_THRESHOLD_SECONDS && !dismissed) {
device_system_time = format_time(new Date(daemon_time_response.system_time));
device_adjusted_time = format_time(new Date(daemon_time_response.adjusted_time));
browser_time = format_time(browser_now);
has_offset = daemon_time_response.offset_seconds !== 0;
// Calculate offset needed: browser_time - daemon_system_time
computed_offset = Math.round((browser_now.getTime() - daemon_system_ms) / 1000);
show_alert = true;
}
} catch (err) {
console.error('Failed to check clock drift:', err);
}
check_completed = true;
}
function dismiss() {
show_alert = false;
dismissed = true;
}
// Check clock drift on component mount
$effect(() => {
check_clock_drift();
});
</script>
{#if show_alert}
<div
class="bg-yellow-100 border-yellow-400 drop-shadow p-4 flex flex-col gap-2 border rounded-md"
>
<span class="text-xl font-bold flex flex-row items-center gap-2 text-yellow-700">
<svg
class="w-6 h-6 text-yellow-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z"
clip-rule="evenodd"
/>
</svg>
Clock Mismatch Detected
</span>
<p>
Rayhunter's clock doesn't match your browser's, and may be incorrect. This can happen if
Rayhunter is unable to get the correct time from the internet. Consider synchronizing
your browser's clock with the button below, or using another SIM card for better
results.
</p>
<table class="w-fit">
<tbody>
<tr>
<td class="pr-2">Rayhunter clock (system):</td>
<td class="font-mono">{device_system_time}</td>
</tr>
{#if has_offset}
<tr>
<td class="pr-2">Rayhunter clock (adjusted):</td>
<td class="font-mono">{device_adjusted_time}</td>
</tr>
{/if}
<tr>
<td class="pr-2">Browser clock:</td>
<td class="font-mono">{browser_time}</td>
</tr>
</tbody>
</table>
<p>Copy browser clock to device?</p>
<div class="flex flex-row gap-2 justify-end">
<button
class="font-medium py-2 px-4 rounded-md border border-gray-400 hover:bg-yellow-200"
onclick={dismiss}
>
Dismiss
</button>
<ApiRequestButton
url="/api/time-offset"
label="Sync Clock"
loadingLabel="Syncing..."
variant="green"
jsonBody={{ offset_seconds: computed_offset }}
onclick={dismiss}
errorMessage="Error syncing clock"
/>
</div>
</div>
{/if}

View File

@@ -1,15 +1,18 @@
<script lang="ts">
import { get_config, set_config, type Config } from '../utils.svelte';
import { get_config, set_config, test_notification, type Config } from '../utils.svelte';
let config = $state<Config | null>(null);
let loading = $state(false);
let saving = $state(false);
let testingNotification = $state(false);
let message = $state('');
let messageType = $state<'success' | 'error' | null>(null);
let testMessage = $state('');
let testMessageType = $state<'success' | 'error' | null>(null);
let showConfig = $state(false);
async function loadConfig() {
async function load_config() {
try {
loading = true;
config = await get_config();
@@ -23,7 +26,7 @@
}
}
async function saveConfig() {
async function save_config() {
if (!config) return;
try {
@@ -40,10 +43,25 @@
}
}
// Load config when first shown
async function send_test_notification() {
try {
testingNotification = true;
testMessage = '';
testMessageType = null;
await test_notification();
testMessage = 'Test notification sent successfully!';
testMessageType = 'success';
} catch (error) {
testMessage = `${error}`;
testMessageType = 'error';
} finally {
testingNotification = false;
}
}
$effect(() => {
if (showConfig && !config) {
loadConfig();
load_config();
}
});
</script>
@@ -73,7 +91,7 @@
class="space-y-4"
onsubmit={(e) => {
e.preventDefault();
saveConfig();
save_config();
}}
>
<div>
@@ -89,7 +107,12 @@
<option value={1}>1 - Subtle mode (colored line)</option>
<option value={2}>2 - Demo mode (orca gif)</option>
<option value={3}>3 - EFF logo</option>
<option value={4}>4 - High visibility (full screen color)</option>
</select>
<p class="text-xs text-gray-500 mt-1">
Note: Rayhunter draws over the device's native UI, so some flickering is
expected
</p>
</div>
<div>
@@ -138,6 +161,49 @@
bind:value={config.ntfy_url}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
Test button below uses the saved configuration URL, not the input above
</p>
</div>
<div>
<button
type="button"
onclick={send_test_notification}
disabled={testingNotification}
class="bg-rayhunter-blue hover:bg-rayhunter-dark-blue disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
>
{#if testingNotification}
<div
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
Sending...
{:else}
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
></path>
</svg>
Send Test Notification
{/if}
</button>
{#if testMessage}
<div
class="mt-2 p-2 rounded text-sm {testMessageType === 'error'
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'}"
>
{testMessage}
</div>
{/if}
</div>
<div class="space-y-2">

View File

@@ -12,7 +12,7 @@
name: string;
} = $props();
function confirmDelete() {
function confirm_delete() {
if (window.confirm(prompt)) {
user_action_req('POST', url, 'Unable to delete recording ' + name);
}
@@ -21,7 +21,7 @@
<button
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
onclick={confirmDelete}
onclick={confirm_delete}
aria-label="delete"
>
<p>{text}</p>

View File

@@ -19,7 +19,7 @@
analysis_status === AnalysisStatus.Queued || analysis_status === AnalysisStatus.Running
);
async function handleReAnalyze() {
async function handle_re_analyze() {
// Update the entry directly for immediate UI feedback
entry.analysis_status = AnalysisStatus.Queued;
entry.analysis_report = undefined;
@@ -33,7 +33,7 @@
loadingLabel="Analyzing..."
disabled={is_processing}
variant="blue"
onclick={handleReAnalyze}
onclick={handle_re_analyze}
ariaLabel="re-analyze"
errorMessage="Error re-analyzing recoding"
>

View File

@@ -5,7 +5,7 @@ import { breakpoints } from '../../theme';
type Breakpoint = keyof typeof breakpoints;
// Store that tracks if a specific breakpoint matches
export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean> {
export function create_breakpoint_store(breakpoint: Breakpoint): Readable<boolean> {
return readable<boolean>(false, (set) => {
const width = breakpoints[breakpoint];
const mediaQuery = window.matchMedia(`(min-width: ${width})`);
@@ -23,7 +23,7 @@ export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean>
}
// Create stores for each breakpoint
export const screenIsSmUp: Readable<boolean> = createBreakpointStore('sm');
export const screenIsMdUp: Readable<boolean> = createBreakpointStore('md');
export const screenIsLgUp: Readable<boolean> = createBreakpointStore('lg');
export const screenIsXlUp: Readable<boolean> = createBreakpointStore('xl');
export const screenIsSmUp: Readable<boolean> = create_breakpoint_store('sm');
export const screenIsMdUp: Readable<boolean> = create_breakpoint_store('md');
export const screenIsLgUp: Readable<boolean> = create_breakpoint_store('lg');
export const screenIsXlUp: Readable<boolean> = create_breakpoint_store('xl');

View File

@@ -26,15 +26,18 @@ export interface Config {
analyzers: AnalyzerConfig;
}
export async function req(method: string, url: string): Promise<string> {
const response = await fetch(url, {
method: method,
});
const body = await response.text();
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
const options: RequestInit = { method };
if (json_body !== undefined) {
options.body = JSON.stringify(json_body);
options.headers = { 'Content-Type': 'application/json' };
}
const response = await fetch(url, options);
const responseBody = await response.text();
if (response.status >= 200 && response.status < 300) {
return body;
return responseBody;
} else {
throw new Error(body);
throw new Error(responseBody);
}
}
@@ -42,13 +45,13 @@ export async function req(method: string, url: string): Promise<string> {
export async function user_action_req(
method: string,
url: string,
error_msg: string
error_msg: string,
json_body?: unknown
): Promise<string | undefined> {
try {
return await req(method, url);
return await req(method, url, json_body);
} catch (error) {
if (error instanceof Error) {
console.log('beeeo');
add_error(error, error_msg);
}
return undefined;
@@ -86,3 +89,24 @@ export async function set_config(config: Config): Promise<void> {
throw new Error(error);
}
}
export async function test_notification(): Promise<void> {
const response = await fetch('/api/test-notification', {
method: 'POST',
});
if (!response.ok) {
const error = await response.text();
throw new Error(error);
}
}
export interface TimeResponse {
system_time: string;
adjusted_time: string;
offset_seconds: number;
}
export async function get_daemon_time(): Promise<TimeResponse> {
return JSON.parse(await req('GET', '/api/time'));
}

View File

@@ -10,6 +10,7 @@
import RecordingControls from '$lib/components/RecordingControls.svelte';
import ConfigForm from '$lib/components/ConfigForm.svelte';
import ActionErrors from '$lib/components/ActionErrors.svelte';
import ClockDriftAlert from '$lib/components/ClockDriftAlert.svelte';
import LogView from '$lib/components/LogView.svelte';
let manager: AnalysisManager = new AnalysisManager();
@@ -102,6 +103,7 @@
/>
</svg>
</button>
<div class="w-px bg-white/30 self-stretch"></div>
<a
class="flex flex-row gap-1 group"
href="https://github.com/EFForg/rayhunter/issues"
@@ -148,6 +150,26 @@
/>
</svg>
</a>
<a
class="flex flex-row gap-1 group"
href="https://supporters.eff.org/donate"
target="_blank"
>
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Donate</span>
<svg
class="w-6 h-6 text-white group-hover:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="m12.75 20.66 6.184-7.098c2.677-2.884 2.559-6.506.754-8.705-.898-1.095-2.206-1.816-3.72-1.855-1.293-.034-2.652.43-3.963 1.537-1.31-1.108-2.67-1.571-3.962-1.537-1.515.04-2.823.76-3.72 1.855-1.806 2.2-1.924 5.821.753 8.705l6.184 7.098.245.281a.75.75 0 0 0 1.09 0l.246-.281Z"
/>
</svg>
</a>
</div>
</div>
<div class="m-4 xl:mx-8 flex flex-col gap-4">
@@ -186,6 +208,7 @@
</div>
{/if}
<ActionErrors />
<ClockDriftAlert />
{#if loaded}
<div class="flex flex-col lg:flex-row gap-4">
{#if current_entry}

File diff suppressed because it is too large Load Diff

1
dist/config.toml.in vendored
View File

@@ -12,6 +12,7 @@ colorblind_mode = false
# 1 = Subtle mode, display a colored line at the top of the screen when rayhunter is running (green=running, white=paused, red=warnings)
# 2 = Demo Mode, display a fun orca gif
# 3 = display the EFF logo
# 4 = High Visibility mode, fill the entire screen with the status color (green=running, white=paused, red=warnings)
#
# TP-Link with one-bit display:
# 0 = invisible mode

View File

@@ -1,9 +1,10 @@
# 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)
@@ -21,5 +22,3 @@
- [Wingtech CT2MHS01](./wingtech-ct2mhs01.md)
- [PinePhone and PinePhone Pro](./pinephone.md)
- [Moxee Hotspot](./moxee.md)
- [Support, feedback, and community](./support-feedback-community.md)
- [Frequently Asked Questions](./faq.md)

View File

@@ -9,7 +9,8 @@ Through web UI you can set:
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording.
- *Demo mode (orca gif)*, which shows image of orcas *and* colored line.
- *EFF logo*, which shows EFF logo and *and* colored line.
- *EFF logo*, which shows EFF logo *and* colored line.
- *High visibility (full screen color)*: fills the entire screen with the status color (green for recording, red for warnings, white for paused).
- **Device Input Mode**, which defines behavior of built-in power button of the device. *Device Input Mode* could be:
- *Disable button control*: built-in power button of the device is not used by Rayhunter.
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.

View File

@@ -24,18 +24,27 @@ If you want to use a non-Verizon SIM card you will probably need an unlocked dev
### How do I re-enable USB tethering after installing Rayhunter?
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
If you have installed with `./installer orbic-usb`, you might find that USB
tethering is now disabled. If you have run `./installer orbic`, this section is not
relevant as it does not use or touch USB.
[First obtain a shell](./orbic.md#shell), then:
```sh
./installer util shell "echo 9 > /usrdata/mode.cfg"
./installer util shell reboot
# inside of Orbic's shell:
echo 9 > /usrdata/mode.cfg
reboot
```
Make sure USB tethering is also enabled in the Orbic's UI.
To disable tethering again:
```sh
./installer util shell "echo 3 > /usrdata/mode.cfg"
./installer util shell reboot
# inside of Orbic's shell:
echo 3 > /usrdata/mode.cfg
reboot
```
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
@@ -43,16 +52,16 @@ See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB g
### How do I disable the WiFi hotspot on the Orbic RC400L?
To disable both WiFi bands:
To disable both WiFi bands, [first obtain a shell](./orbic.md#shell), then:
```sh
adb shell
/bin/rootshell -c "sed -i 's/<wlan><Feature><state>1<\/state>/<wlan><Feature><state>0<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
# inside of Orbic's shell:
sed -i 's/<wlan><Feature><state>1<\/state>/<wlan><Feature><state>0<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot
```
To re-enable WiFi:
```sh
adb shell
/bin/rootshell -c "sed -i 's/<wlan><Feature><state>0<\/state>/<wlan><Feature><state>1<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
# inside of Orbic's shell:
sed -i 's/<wlan><Feature><state>0<\/state>/<wlan><Feature><state>1<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot
```

View File

@@ -39,7 +39,7 @@ This heuristic will also issue a notification every time your identity is sent t
This analyzer tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
### LTE SIB6/7 Downgrade
### LTE SIB6/7 Downgrade (v2)
This analyzer tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
@@ -49,7 +49,7 @@ This attack exploits the fact that SIB broadcast messages are not encrypted or a
SIB6 is used for cell reselection to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristic may trigger false positives. In that case you should consider disabling it. However this heuristic has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
This heuristic is useful even in countries where 2g is still prevalent. A well behaved tower should always advertise its other 4g neighbors at a higher priority than 2g/3g neighbors. (Older versions of this heuristic were prone to false positives.)
### Null Cipher

View File

@@ -3,5 +3,8 @@
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)

View File

@@ -41,6 +41,9 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
```bash
# For Orbic:
./installer orbic --admin-password 'mypassword'
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
# may be required if different from the default.
# Or install over USB if you want ADB and a root shell (not recommended for most users)
./installer orbic-usb
@@ -48,7 +51,8 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
./installer tplink
```
* On Verizon Orbic, the password is the WiFi password.
* On Verizon Orbic, the password is the one used to login to the device's admin menu, and the default is the WiFi password.
* ***Note:*** If you have changed the device username, password, or IP address from their default values, these must be provided as arguments to the installer command above.
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
* On Moxee-brand devices, check under the battery for the password.
* You can reset the password by pressing the button under the back case until the unit restarts.

View File

@@ -47,12 +47,11 @@ cargo build-daemon-firmware-devel
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
# Build rootshell
cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
cargo build-rootshell-firmware-devel
# Replace 'orbic' with your device type if different.
# A list of possible values can be found with 'cargo run --bin installer help'.
# Use FILE_RAYHUNTER_DAEMON to specify the daemon binary path when using development builds:
FILE_RAYHUNTER_DAEMON=$PWD/target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon cargo run -p installer --bin installer orbic
FIRMWARE_PROFILE=firmware-devel cargo run -p installer --bin installer orbic
```
### If you're on Windows or can't run the install scripts

View File

@@ -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-network --admin-password 'mypassword'
./installer orbic --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-start-telnet
./installer util orbic-shell
```

View File

@@ -6,7 +6,8 @@ It is also sometimes sold under the brand Kajeet RC400L. This is the exact same
You can buy an Orbic [using bezos
bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y),
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l). You should not
pay more than 30 USD for such a device (without shipping).
[Please check whether the Orbic works in your country](https://www.frequencycheck.com/countries/), and whether the Orbic RC400L supports the right frequency bands for your purpose before buying.
@@ -37,12 +38,11 @@ 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-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.
enabled. Instead you can use `./installer util orbic-shell`.
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,

View File

@@ -2,7 +2,15 @@
If you're using Rayhunter (or trying to), we'd love to hear from you! Check out one of the following forums for contacting the Rayhunter developers and community:
* If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (QMDL and PCAP logs) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
* If you're having issues installing or using Rayhunter, please [open an issue](https://github.com/EFForg/rayhunter/issues) on our Github repo.
* If you've received a Rayhunter warning, please send your Rayhunter data captures (the ZIP file) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version.
Note that the recording files are sensitive data and contain location
information, so we strongly recommend against posting them to publicly.
If you're unfamiliar with Signal, feel free to check out our [Security Self
Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
* If you're having issues installing or using Rayhunter, consider checking the [Frequently Asked Questions](./faq.md) page for answers to common questions.
* If your question isn't answered there, please [open an issue](https://github.com/EFForg/rayhunter/issues) on our Github repo.
* If you'd like to propose a feature, heuristic, or device for Rayhunter, [start a discussion](https://github.com/EFForg/rayhunter/discussions) in our Github repo
* For anything else, join us in the `#rayhunter` or `#rayhunter-developers` channel of [EFF's Mattermost](https://opensource.eff.org/signup_user_complete/?id=r1b6cnta9bysxk6im3kuabiu1y&md=link&sbr=su) instance to chat!

View File

@@ -35,6 +35,7 @@ You can get your TP-Link M7350 from:
* First check for used offers on local sites, sometimes it's much cheaper there.
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350).
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313).
* Can also be found sold as the 'Vodafone Pocket Wifi 5' in Australia
## Installation & Usage
@@ -42,11 +43,10 @@ Follow the [release installation guide](./installing-from-release.md). Substitut
## Obtaining a shell
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
You can obtain a root shell with the following command:
```sh
./installer util tplink-start-telnet
telnet 192.168.0.1
./installer util tplink-shell
```
## 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-start-telnet`.
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`.
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

View File

@@ -1,23 +1,25 @@
# Uninstalling
There is no automated uninstallation routine, so this page documents the routine for some devices.
## Orbic
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`.
Run `./installer util orbic-shell --admin-password mypassword`. Refer to the
installation instructions for how to find out the admin password.
Once in a rootshell, run:
Inside, run:
```shell
echo 3 > /usrdata/mode.cfg
echo 3 > /usrdata/mode.cfg # only relevant if you previously installed via ADB installer
rm -rf /data/rayhunter /etc/init.d/rayhunter_daemon /bin/rootshell
reboot
```
Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabled mode.
Your device is now Rayhunter-free, and should no longer be rooted.
## TPLink
1. Run `./installer util tplink-start-telnet`
2. Telnet into the device `telnet 192.168.0.1`
1. Run `./installer util tplink-shell` to obtain rootshell on the device.
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 Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/src-tauri/binaries

View File

@@ -0,0 +1 @@
package-lock.json

15
installer-gui/.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

35
installer-gui/README.md Normal file
View File

@@ -0,0 +1,35 @@
# 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.

View File

@@ -0,0 +1,42 @@
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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"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 Normal file
View File

@@ -0,0 +1,3 @@
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View File

@@ -0,0 +1,24 @@
[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" }

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["core:default", "opener:default"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,34 @@
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");
}

View File

@@ -0,0 +1,6 @@
// 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()
}

View File

@@ -0,0 +1,34 @@
{
"$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"
]
}
}

View File

@@ -0,0 +1,7 @@
@import 'tailwindcss';
@theme {
--color-rayhunter-blue: #4e4eb1;
--color-rayhunter-dark-blue: #3f3da0;
--color-rayhunter-green: #94ea18;
}

View File

@@ -0,0 +1,12 @@
<!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>

View File

@@ -0,0 +1,6 @@
<script>
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View File

@@ -0,0 +1,5 @@
// 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;

View File

@@ -0,0 +1,95 @@
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,15 @@
// 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;

View File

@@ -0,0 +1,19 @@
{
"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
}

View File

@@ -0,0 +1,33 @@
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/**'],
},
},
}));

View File

@@ -1,15 +1,23 @@
[package]
name = "installer"
version = "0.8.0"
version = "0.10.1"
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.10.1"
bytes = "1.11.1"
clap = { version = "4.5.37", features = ["derive"] }
env_logger = "0.11.8"
hyper = "1.6.0"
@@ -23,8 +31,12 @@ 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(target_os = "linux")'.dependencies.adb_client]
[target.'cfg(unix)'.dependencies]
termios = "0.3"
[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client]
git = "https://github.com/EFForg/adb_client.git"
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
default-features = false

View File

@@ -1,18 +1,19 @@
use core::str;
use std::path::Path;
use std::process::exit;
fn main() {
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
let include_dir = Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../target/armv7-unknown-linux-musleabihf/firmware/"
));
set_binary_var(include_dir, "FILE_ROOTSHELL", "rootshell");
set_binary_var(include_dir, "FILE_RAYHUNTER_DAEMON", "rayhunter-daemon");
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| "firmware".to_string());
let include_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../target/armv7-unknown-linux-musleabihf")
.join(&profile);
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
set_binary_var(&include_dir, "FILE_RAYHUNTER_DAEMON", "rayhunter-daemon");
}
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
println!("cargo::rerun-if-env-changed={var}");
if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::create_dir_all(&out_dir).unwrap();
@@ -23,6 +24,7 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
}
if std::env::var_os(var).is_none() {
let binary = include_dir.join(file);
println!("cargo::rerun-if-changed={}", binary.display());
if !binary.exists() {
println!(
"cargo::error=Firmware binary {file} not present at {}",
@@ -31,6 +33,5 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
exit(0);
}
println!("cargo::rustc-env={var}={}", binary.display());
println!("cargo::rerun-if-changed={}", binary.display());
}
}

View File

@@ -1,142 +0,0 @@
$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

View File

@@ -0,0 +1,68 @@
use std::future::Future;
use std::net::SocketAddr;
use anyhow::Result;
use crate::output::println;
/// Abstraction for device communication (telnet or ADB)
pub trait DeviceConnection {
/// Run a shell command and return its output
fn run_command(&mut self, command: &str) -> impl Future<Output = Result<String>> + Send;
/// Write a file to the device
fn write_file(&mut self, path: &str, content: &[u8])
-> impl Future<Output = Result<()>> + Send;
}
/// Check if a file exists using a DeviceConnection
pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
conn.run_command(&format!("test -f {path} && echo exists || echo missing"))
.await
.map(|output| output.contains("exists"))
.unwrap_or(false)
}
/// Shared config installation logic
pub async fn install_config<C: DeviceConnection>(
conn: &mut C,
config_path: &str,
device_type: &str,
reset_config: bool,
) -> Result<()> {
if reset_config || !file_exists(conn, config_path).await {
let config = crate::CONFIG_TOML.replace(
r#"#device = "orbic""#,
&format!(r#"device = "{device_type}""#),
);
conn.write_file(config_path, config.as_bytes()).await?;
} else {
println!("Config file already exists, skipping (use --reset-config to overwrite)");
}
Ok(())
}
/// Telnet-based connection wrapper
pub struct TelnetConnection {
pub addr: SocketAddr,
pub wait_for_prompt: bool,
}
impl TelnetConnection {
pub fn new(addr: SocketAddr, wait_for_prompt: bool) -> Self {
Self {
addr,
wait_for_prompt,
}
}
}
impl DeviceConnection for TelnetConnection {
async fn run_command(&mut self, command: &str) -> Result<String> {
crate::util::telnet_send_command_with_output(self.addr, command, self.wait_for_prompt).await
}
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
crate::util::telnet_send_file(self.addr, path, content, self.wait_for_prompt).await
}
}

359
installer/src/lib.rs Normal file
View File

@@ -0,0 +1,359 @@
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
}

View File

@@ -1,260 +1,6 @@
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) = run().await {
if let Err(e) = installer::main_cli().await {
eprintln!("{e:?}");
std::process::exit(1);
}

View File

@@ -1,7 +1,7 @@
#[cfg(target_os = "windows")]
use std::io::stdin;
use std::io::{ErrorKind, Write};
use std::io::ErrorKind;
use std::path::Path;
use std::time::Duration;
@@ -12,8 +12,10 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use sha2::{Digest, Sha256};
use tokio::time::sleep;
use crate::util::{echo, open_usb_device};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{DeviceConnection, install_config};
use crate::output::{print, println};
use crate::util::open_usb_device;
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
Make sure your device is plugged in and turned on.
@@ -45,6 +47,21 @@ const PRODUCT_ID: u16 = 0xf601;
const INTERFACE: u8 = 1;
/// ADB-based connection wrapper for DeviceConnection trait
pub struct AdbConnection<'a> {
device: &'a mut ADBUSBDevice,
}
impl DeviceConnection for AdbConnection<'_> {
async fn run_command(&mut self, command: &str) -> Result<String> {
adb_command(self.device, &["sh", "-c", command])
}
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
install_file(self.device, path, content).await
}
}
#[cfg(target_os = "windows")]
const RNDIS_INTERFACE: u8 = 0;
@@ -54,13 +71,13 @@ const RNDIS_INTERFACE: u8 = 1;
#[cfg(target_os = "windows")]
async fn confirm() -> Result<bool> {
println!("{}", WINDOWS_WARNING);
echo!("Do you wish to proceed? Enter 'yes' to install> ");
print!("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() -> Result<()> {
pub async fn install(reset_config: bool) -> Result<()> {
println!(
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes."
);
@@ -75,13 +92,13 @@ pub async fn install() -> Result<()> {
}
let mut adb_device = force_debug_mode().await?;
echo!("Installing rootshell... ");
print!("Installing rootshell... ");
setup_rootshell(&mut adb_device).await?;
println!("done");
echo!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(adb_device).await?;
print!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(adb_device, reset_config).await?;
println!("done");
echo!("Testing rayhunter... ");
print!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?;
println!("done");
Ok(())
@@ -89,7 +106,7 @@ pub async fn install() -> Result<()> {
pub async fn shell() -> Result<()> {
println!(
"WARNING: The orbic USB installer is likely to go away in a future version of Rayhunter. Consider using ./installer util orbic-start-telnet instead."
"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."
);
println!("opening shell");
@@ -101,11 +118,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()?;
echo!("ADB enabled, waiting for reboot... ");
print!("ADB enabled, waiting for reboot... ");
let mut adb_device = get_adb().await?;
adb_setup_serial(&mut adb_device).await?;
println!("it's alive!");
echo!("Waiting for atfwd_daemon to startup... ");
print!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
println!("done");
Ok(adb_device)
@@ -126,7 +143,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
@@ -136,14 +153,20 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
rayhunter_daemon_bin,
)
.await?;
install_file(
&mut adb_device,
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"orbic\"")
.as_bytes(),
)
.await?;
{
let mut conn = AdbConnection {
device: &mut adb_device,
};
install_config(
&mut conn,
"/data/rayhunter/config.toml",
"orbic",
reset_config,
)
.await?;
}
install_file(
&mut adb_device,
"/etc/init.d/rayhunter_daemon",
@@ -159,7 +182,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
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");
echo!("Waiting for reboot... ");
print!("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 {

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -8,9 +7,14 @@ 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::util::{echo, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
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;
#[derive(Deserialize, Debug)]
struct ExploitResponse {
@@ -101,10 +105,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 (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 & #"}"#)
// 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 & #"}}"#
))
.send()
.await
.context("failed to start telnet")?
@@ -128,7 +132,7 @@ pub async fn start_telnet(
anyhow::bail!("--admin-password is required");
};
echo!("Logging in and starting telnet... ");
print!("Logging in and starting telnet... ");
login_and_exploit(admin_ip, admin_username, admin_password).await?;
println!("done");
@@ -139,6 +143,7 @@ pub async fn install(
admin_ip: String,
admin_username: String,
admin_password: Option<String>,
reset_config: bool,
) -> Result<()> {
let Some(admin_password) = admin_password else {
eprintln!(
@@ -154,19 +159,19 @@ pub async fn install(
anyhow::bail!("exiting");
};
echo!("Logging in and starting telnet... ");
print!("Logging in and starting telnet... ");
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
println!("done");
echo!("Waiting for telnet to become available... ");
print!("Waiting for telnet to become available... ");
wait_for_telnet(&admin_ip).await?;
println!("done");
setup_rayhunter(&admin_ip).await
setup_rayhunter(&admin_ip, reset_config).await
}
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let timeout = Duration::from_secs(60);
let start_time = std::time::Instant::now();
@@ -186,8 +191,8 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
// Remount filesystem as read-write to allow modifications
@@ -210,13 +215,12 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
)
.await?;
telnet_send_file(
addr,
let mut conn = TelnetConnection::new(addr, false);
install_config(
&mut conn,
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace(r#"#device = "orbic""#, r#"device = "orbic""#)
.as_bytes(),
false,
"orbic",
reset_config,
)
.await?;
@@ -270,3 +274,16 @@ async fn setup_rayhunter(admin_ip: &str) -> 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
}

112
installer/src/output.rs Normal file
View File

@@ -0,0 +1,112 @@
//! 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;

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::path::Path;
use std::time::Duration;
@@ -10,8 +9,10 @@ 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::util::{echo, open_usb_device};
use crate::output::{print, println};
use crate::util::open_usb_device;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
const USB_VENDOR_ID: u16 = 0x2C7C;
@@ -19,48 +20,54 @@ const USB_PRODUCT_ID: u16 = 0x125;
const USB_INTERFACE_NUMBER: u8 = 2;
pub async fn install() -> Result<()> {
echo!("Unlocking modem ... ");
print!("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");
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?;
run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?;
run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?;
adb.install_file(
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
.await?;
adb.write_file(
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"pinephone\"")
.as_bytes(),
)?;
adb.install_file(
)
.await?;
adb.write_file(
"/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(),
)?;
adb.install_file(
)
.await?;
adb.write_file(
"/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"),
)?;
adb.run_command(
&["chmod", "755", "/etc/init.d/rayhunter_daemon"],
)
.await?;
run_command_expect(
&mut adb,
"chmod 755 /etc/init.d/rayhunter_daemon",
"exit code 0",
)?;
adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?;
)
.await?;
run_command_expect(&mut adb, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?;
println!("Rebooting device and waiting 30 seconds for it to start up.");
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
run_command_expect(&mut adb, "shutdown -r -t 1 now", "exit code 0").await?;
sleep(Duration::from_secs(30)).await;
echo!("Unlocking modem ... ");
print!("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");
echo!("Testing rayhunter ... ");
print!("Testing rayhunter ... ");
test_rayhunter(&mut adb).await?;
println!("ok");
println!("rayhunter is running on the modem. Use adb to access the web interface.");
@@ -68,6 +75,19 @@ pub async fn install() -> Result<()> {
Ok(())
}
/// Helper to run a command and check for expected output
async fn run_command_expect(
adb: &mut ADBUSBDevice,
command: &str,
expected_output: &str,
) -> Result<()> {
let output = adb.run_command(command).await?;
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
}
Ok(())
}
struct Qusbcfg {
vendor_id: u16,
product_id: u16,
@@ -175,30 +195,19 @@ pub async fn stop_adb() -> Result<()> {
Ok(())
}
trait Install {
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>;
fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>;
}
impl Install for ADBUSBDevice {
/// Run an adb shell command, append '; echo exit code $?' to the command and verify its output.
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> {
impl DeviceConnection for ADBUSBDevice {
/// Run an adb shell command, append '; echo exit code $?' to the command and return output.
async fn run_command(&mut self, command: &str) -> Result<String> {
let mut buf = Vec::<u8>::new();
let mut cmd = Vec::<&str>::new();
cmd.extend_from_slice(command);
cmd.extend_from_slice(&[";", "echo", "exit code $?"]);
let cmd = ["sh", "-c", &format!("{command}; echo exit code $?")];
self.shell_command(&cmd, &mut buf)?;
let output = String::from_utf8_lossy(&buf);
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
}
Ok(())
Ok(String::from_utf8_lossy(&buf).into_owned())
}
/// Transfer a file to the modem's filesystem with adb push.
/// Validates the file sends successfully to /tmp before overwriting the destination.
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
echo!("Sending file {dest} ... ");
async fn write_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
print!("Sending file {dest} ... ");
let file_name = Path::new(dest)
.file_name()
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
@@ -208,8 +217,16 @@ impl Install for ADBUSBDevice {
let push_tmp_path = format!("/tmp/{file_name}");
let file_hash = md5_compute(payload);
self.push(&mut payload, &push_tmp_path)?;
self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?;
self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?;
let output = self.run_command(&format!("md5sum {push_tmp_path}")).await?;
if !output.contains(&format!("{file_hash:x}")) {
bail!("{:x} not found in: {output}", file_hash);
}
let output = self
.run_command(&format!("mv {push_tmp_path} {dest}"))
.await?;
if !output.contains("exit code 0") {
bail!("exit code 0 not found in: {output}");
}
println!("ok");
Ok(())
}

View File

@@ -4,7 +4,6 @@
/// WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP
/// WT_PRODUCTION_VERSION=TMOHS1_00.05.20
/// WT_HARDWARE_VERSION=89527_1_11
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -13,7 +12,8 @@ use anyhow::Result;
use tokio::time::sleep;
use crate::TmobileArgs as Args;
use crate::util::{echo, http_ok_every, telnet_send_command, telnet_send_file};
use crate::output::{print, println};
use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
use crate::wingtech::start_telnet;
pub async fn install(
@@ -26,12 +26,12 @@ pub async fn install(
}
async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
echo!("Starting telnet ... ");
print!("Starting telnet ... ");
start_telnet(&admin_ip, &admin_password).await?;
sleep(Duration::from_millis(200)).await;
println!("ok");
echo!("Connecting via telnet to {admin_ip} ... ");
print!("Connecting via telnet to {admin_ip} ... ");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
println!("ok");
@@ -96,7 +96,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
telnet_send_command(addr, "reboot", "exit code 0", true).await?;
sleep(Duration::from_secs(30)).await;
echo!("Testing rayhunter ... ");
print!("Testing rayhunter ... ");
let max_failures = 10;
http_ok_every(
format!("http://{admin_ip}:8080/index.html"),

View File

@@ -18,7 +18,9 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::InstallTpLink;
use crate::util::{telnet_send_command, telnet_send_file};
use crate::connection::{TelnetConnection, install_config};
use crate::output::println;
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
@@ -27,10 +29,11 @@ pub async fn main_tplink(
skip_sdcard,
admin_ip,
sdcard_path,
reset_config,
}: InstallTpLink,
) -> Result<(), Error> {
let is_v3 = start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await
}
#[derive(Deserialize)]
@@ -110,6 +113,7 @@ async fn tplink_run_install(
admin_ip: String,
mut sdcard_path: String,
is_v3: bool,
reset_config: bool,
) -> Result<(), Error> {
println!("Connecting via telnet to {admin_ip}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
@@ -180,15 +184,9 @@ async fn tplink_run_install(
)
.await?;
telnet_send_file(
addr,
&format!("{sdcard_path}/config.toml"),
crate::CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"tplink\"")
.as_bytes(),
true,
)
.await?;
let mut conn = TelnetConnection::new(addr, true);
let config_path = format!("{sdcard_path}/config.toml");
install_config(&mut conn, &config_path, "tplink", reset_config).await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
@@ -378,10 +376,18 @@ fn get_rayhunter_daemon(sdcard_path: &str) -> String {
// specific to a particular hardware revision here.
crate::RAYHUNTER_DAEMON_INIT.replace(
"#RAYHUNTER-PRESTART",
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
&format!(
"(mount /dev/mmcblk0p1 {sdcard_path} || true) 2>&1 | tee /tmp/rayhunter-mount.log"
),
)
}
/// Root the TP-Link device and open an interactive shell
pub async fn shell(admin_ip: &str) -> Result<(), Error> {
start_telnet(admin_ip).await?;
interactive_shell(admin_ip, 23, true).await
}
#[test]
fn test_get_rayhunter_daemon() {
let s = get_rayhunter_daemon("/media/card");

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -10,13 +9,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
macro_rules! echo {
($($arg:tt)*) => {
print!($($arg)*);
let _ = std::io::stdout().flush();
};
}
pub(crate) use echo;
use crate::output::{print, println};
#[cfg(unix)]
use std::os::fd::AsRawFd;
pub async fn telnet_send_command_with_output(
addr: SocketAddr,
@@ -91,7 +87,7 @@ pub async fn telnet_send_file(
payload: &[u8],
wait_for_prompt: bool,
) -> Result<()> {
echo!("Sending file {filename}... ");
print!("Sending file {filename} ... ");
let nc_output = {
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
@@ -122,7 +118,7 @@ pub async fn telnet_send_file(
break;
}
echo!("attempt {attempts}... ");
print!("attempt {attempts}... ");
}
{
@@ -216,6 +212,7 @@ pub async fn http_ok_every(
}
/// General function to open a USB device
#[cfg(not(target_os = "android"))]
pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
let devices = match nusb::list_devices() {
Ok(d) => d,
@@ -231,3 +228,82 @@ pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
}
Ok(None)
}
/// Open an interactive shell to a device
///
/// Connects to a shell service on the device and forwards stdin/stdout bidirectionally.
pub async fn interactive_shell(admin_ip: &str, shell_port: u16, raw_mode: bool) -> Result<()> {
let shell_addr = SocketAddr::from_str(&format!("{admin_ip}:{shell_port}"))?;
let mut stream = TcpStream::connect(shell_addr)
.await
.context("Failed to connect to shell. Make sure the device is reachable.")?;
let stdin = tokio::io::stdin();
#[cfg(unix)]
let raw_terminal_guard = if raw_mode {
Some(RawTerminal::new(stdin.as_raw_fd())?)
} else {
None
};
// suppress "unused variable" lint
#[cfg(not(unix))]
let _used = raw_mode;
let mut stdio = tokio::io::join(stdin, tokio::io::stdout());
let _ = tokio::io::copy_bidirectional(&mut stream, &mut stdio).await;
// hitting ctrl-d will not print a trailing newline on tplink at least, which messes up the
// next prompt
println!();
// The current_thread runtime in tokio will block forever until stdin receives a read error. To
// work around this cleanup issue we just exit directly from here.
//
// This is documented as a flaw in tokio::io::stdin()'s own docs, but the recommended
// workaround to spawn your own OS thread doesn't work.
//
// For some reason this only happens when the terminal is being put in raw mode (removing
// RawTerminal fixes it)
//
// We have to drop the RawTerminal guard before exiting, otherwise we will
// mess up the terminal.
#[cfg(unix)]
drop(raw_terminal_guard);
std::process::exit(0)
}
#[cfg(unix)]
struct RawTerminal {
fd: std::os::fd::RawFd,
original_termios: termios::Termios,
}
#[cfg(unix)]
impl RawTerminal {
fn new(fd: std::os::fd::RawFd) -> Result<Self> {
// put terminal in raw mode so that arrow keys, tab etc are correctly forwarded to the
// device's shell
let original_termios = termios::Termios::from_fd(fd)?;
let mut new_termios = original_termios;
// set flags on the struct
termios::cfmakeraw(&mut new_termios);
// apply changes
termios::tcsetattr(fd, termios::TCSANOW, &new_termios)?;
Ok(RawTerminal {
fd,
original_termios,
})
}
}
#[cfg(unix)]
impl Drop for RawTerminal {
fn drop(&mut self) {
let _ = termios::tcsetattr(self.fd, termios::TCSANOW, &self.original_termios);
}
}

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::path::Path;
/// Installer for the Uz801 hotspot.
///
@@ -15,30 +14,30 @@ use md5::compute as md5_compute;
use tokio::time::sleep;
use crate::Uz801Args as Args;
use crate::util::echo;
use crate::output::{print, println};
pub async fn install(Args { admin_ip }: Args) -> Result<()> {
run_install(admin_ip).await
}
async fn run_install(admin_ip: String) -> Result<()> {
echo!("Activating USB debugging backdoor... ");
print!("Activating USB debugging backdoor... ");
activate_usb_debug(&admin_ip).await?;
println!("ok");
echo!("Waiting for device reboot and ADB connection... ");
print!("Waiting for device reboot and ADB connection... ");
let mut adb_device = wait_for_adb().await?;
println!("ok");
echo!("Installing rayhunter files... ");
print!("Installing rayhunter files... ");
install_rayhunter_files(&mut adb_device).await?;
println!("ok");
echo!("Modifying startup script... ");
print!("Modifying startup script... ");
modify_startup_script(&mut adb_device).await?;
println!("ok");
echo!("Rebooting the device... ");
print!("Rebooting the device... ");
let _ = adb_device.reboot(adb_client::RebootType::System);
println!("ok");
@@ -55,7 +54,7 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
let origin = format!("http://{admin_ip}");
// Check if device is online
echo!("Checking if device is online... ");
print!("Checking if device is online... ");
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()?;

Some files were not shown because too many files have changed in this diff Show More