Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7977a01a88 | ||
|
|
78dd2f74a4 | ||
|
|
dd70a2a15d | ||
|
|
81a193959c | ||
|
|
7209910c11 | ||
|
|
3615cbf2dd | ||
|
|
61793179e5 | ||
|
|
cdc7a46162 | ||
|
|
ffe58ab72b | ||
|
|
7906bf7d67 | ||
|
|
5e4174c9f3 | ||
|
|
2a8fee25f9 | ||
|
|
516e878661 | ||
|
|
5fbc540fa0 | ||
|
|
676cd3c862 | ||
|
|
a8cb363112 | ||
|
|
6172236a3c | ||
|
|
485d1a99f6 | ||
|
|
f6e118a5cc | ||
|
|
4cdc9961d3 | ||
|
|
c18579583c | ||
|
|
565b6d188d | ||
|
|
80f12ffaaa | ||
|
|
3e9af006e1 | ||
|
|
73a5d324c4 | ||
|
|
bb6135c682 | ||
|
|
3b44234ae1 | ||
|
|
9e9fe4d392 | ||
|
|
2c92315125 | ||
|
|
7bc55bf432 | ||
|
|
2a7c5b4365 | ||
|
|
d48d5755c6 | ||
|
|
1cf1d6d5b9 | ||
|
|
c8d1b52ca7 | ||
|
|
04efe7bb75 | ||
|
|
3f3b6168b3 | ||
|
|
992a28af57 | ||
|
|
39c8844967 | ||
|
|
ef006d83a6 | ||
|
|
bc9022530a | ||
|
|
af2445cc38 | ||
|
|
e33f143830 | ||
|
|
f5360b042c | ||
|
|
a16fb9b678 | ||
|
|
3349895a3e | ||
|
|
30b517069a | ||
|
|
4efc2d5db3 | ||
|
|
5e066682b3 | ||
|
|
01aefe25c9 | ||
|
|
e8e9f9366c | ||
|
|
fa346989e6 | ||
|
|
d942545ac3 |
11
.cargo/audit.toml
Normal 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",
|
||||
]
|
||||
218
.github/workflows/main.yml
vendored
@@ -25,18 +25,20 @@ jobs:
|
||||
web_changed: ${{ steps.files_changed.outputs.web_count }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
||||
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
|
||||
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count }}
|
||||
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||
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 [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo
|
||||
then
|
||||
echo "building everything"
|
||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
||||
@@ -44,13 +46,15 @@ jobs:
|
||||
echo web_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo docs_count=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 "installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer/ | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "installer_gui_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer-gui | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -63,6 +67,8 @@ jobs:
|
||||
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 +86,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
|
||||
@@ -104,6 +112,8 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
@@ -124,7 +134,35 @@ 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.installer_gui_changed != '0'
|
||||
# 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 != '0'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -135,11 +173,30 @@ 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 != '0'
|
||||
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'
|
||||
@@ -148,6 +205,8 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: cargo check
|
||||
shell: bash
|
||||
@@ -192,6 +251,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 }}
|
||||
@@ -214,6 +275,8 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
@@ -240,6 +303,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
|
||||
@@ -301,6 +366,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 +380,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 != '0'
|
||||
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 != '0'
|
||||
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 != '0'
|
||||
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 +540,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 +551,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 +559,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"
|
||||
|
||||
|
||||
8
.github/workflows/release.yml
vendored
@@ -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: |
|
||||
|
||||
3473
Cargo.lock
generated
12
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||

|
||||
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts 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.
|
||||
|
||||
→ Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
12
daemon/web/package-lock.json
generated
@@ -2760,9 +2760,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": {
|
||||
@@ -3033,9 +3033,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": {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[Introduction](./introduction.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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -40,9 +40,7 @@ installation routines.
|
||||
## 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,
|
||||
|
||||
@@ -42,11 +42,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 +69,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
1
installer-gui/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json
|
||||
15
installer-gui/.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
34
installer-gui/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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 and then provide the path explicitly:
|
||||
|
||||
```bash
|
||||
cargo build-daemon-firmware-devel
|
||||
|
||||
(cd installer-gui && FILE_RAYHUNTER_DAEMON=$PWD/../target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon 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.
|
||||
42
installer-gui/eslint.config.js
Normal 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',
|
||||
},
|
||||
}
|
||||
);
|
||||
4211
installer-gui/package-lock.json
generated
Normal file
41
installer-gui/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "installer-gui",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"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.9.0",
|
||||
"@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
@@ -0,0 +1,3 @@
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
24
installer-gui/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "installer-gui"
|
||||
version = "0.9.0"
|
||||
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" }
|
||||
3
installer-gui/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
7
installer-gui/src-tauri/capabilities/default.json
Normal 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"]
|
||||
}
|
||||
BIN
installer-gui/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
installer-gui/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
installer-gui/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
installer-gui/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
installer-gui/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
installer-gui/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
installer-gui/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
installer-gui/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
installer-gui/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
installer-gui/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
installer-gui/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
installer-gui/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
installer-gui/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
installer-gui/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
installer-gui/src-tauri/icons/icon.icns
Normal file
BIN
installer-gui/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
installer-gui/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
34
installer-gui/src-tauri/src/lib.rs
Normal 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");
|
||||
}
|
||||
6
installer-gui/src-tauri/src/main.rs
Normal 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()
|
||||
}
|
||||
34
installer-gui/src-tauri/tauri.conf.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
installer-gui/src/app.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-rayhunter-blue: #4e4eb1;
|
||||
--color-rayhunter-dark-blue: #3f3da0;
|
||||
--color-rayhunter-green: #94ea18;
|
||||
}
|
||||
12
installer-gui/src/app.html
Normal 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>
|
||||
6
installer-gui/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
5
installer-gui/src/routes/+layout.ts
Normal 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;
|
||||
95
installer-gui/src/routes/+page.svelte
Normal 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>
|
||||
BIN
installer-gui/static/favicon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
installer-gui/static/rayhunter_text.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
15
installer-gui/svelte.config.js
Normal 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;
|
||||
19
installer-gui/tsconfig.json
Normal 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
|
||||
}
|
||||
33
installer-gui/vite.config.js
Normal 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/**'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,8 +1,16 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "installer"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "installer"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
anyhow = "1.0.98"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
346
installer/src/lib.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use anyhow::{Context, Error};
|
||||
use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use anyhow::bail;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[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(_) => 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 {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
if serial_cmd.root {
|
||||
if !serial_cmd.command.is_empty() {
|
||||
eprintln!("You cannot use --root and specify a command at the same time");
|
||||
std::process::exit(64);
|
||||
}
|
||||
orbic::enable_command_mode()?;
|
||||
} else if serial_cmd.command.is_empty() {
|
||||
eprintln!("Command cannot be an empty string");
|
||||
std::process::exit(64);
|
||||
} else {
|
||||
let cmd = serial_cmd.command.join(" ");
|
||||
match orbic::open_orbic()? {
|
||||
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
|
||||
None => bail!(orbic::ORBIC_NOT_FOUND),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
UtilSubCommand::TplinkShell(options) => {
|
||||
tplink::shell(&options.admin_ip).await.context("\nFailed to open shell on TP-Link device")?;
|
||||
}
|
||||
UtilSubCommand::TplinkSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechSendFile(options) => {
|
||||
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
||||
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to start telnet on the Orbic RC400L")?,
|
||||
UtilSubCommand::OrbicShell(args) => orbic_network::shell(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Type alias for output callback function
|
||||
pub type OutputCallback = Box<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
/// Run the installer with CLI arguments and optional output callback
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use installer;
|
||||
///
|
||||
/// // if the callback is None, stdout/stderr is going to be used
|
||||
/// let result = installer::run_with_callback(
|
||||
/// ["orbic-network", "--admin-password", "12345"],
|
||||
/// Some(Box::new(|output| {
|
||||
/// print!("{}", output);
|
||||
/// }))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn run_with_callback<'a>(
|
||||
args: impl IntoIterator<Item = &'a str>,
|
||||
callback: Option<OutputCallback>,
|
||||
) -> Result<(), Error> {
|
||||
let _guard;
|
||||
if let Some(cb) = callback {
|
||||
_guard = output::set_output_callback(move |s: &str| cb(s));
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to create Tokio runtime")?
|
||||
.block_on(async {
|
||||
let args = std::iter::once("installer").chain(args);
|
||||
match Args::try_parse_from(args) {
|
||||
Ok(parsed_args) => run(parsed_args).await,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the version of the installer
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Run the CLI installer
|
||||
///
|
||||
/// This function is public so the binary can call it, library users should use `run_with_callback`
|
||||
/// instead.
|
||||
pub async fn main_cli() -> Result<(), Error> {
|
||||
let args = Args::parse();
|
||||
run(args).await
|
||||
}
|
||||
@@ -1,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);
|
||||
}
|
||||
|
||||
@@ -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,7 +12,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::time::sleep;
|
||||
|
||||
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};
|
||||
|
||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||
@@ -54,7 +55,7 @@ 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")
|
||||
@@ -75,13 +76,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... ");
|
||||
print!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||
println!("done");
|
||||
echo!("Testing rayhunter... ");
|
||||
print!("Testing rayhunter... ");
|
||||
test_rayhunter(&mut adb_device).await?;
|
||||
println!("done");
|
||||
Ok(())
|
||||
@@ -89,7 +90,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 +102,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)
|
||||
@@ -159,7 +160,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 {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
@@ -9,9 +8,13 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
||||
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
||||
use crate::output::{eprintln, print, println};
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
|
||||
const TELNET_PORT: u16 = 24;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExploitResponse {
|
||||
retcode: u32,
|
||||
@@ -101,10 +104,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 +131,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");
|
||||
|
||||
@@ -154,11 +157,11 @@ 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");
|
||||
|
||||
@@ -166,7 +169,7 @@ pub async fn install(
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -187,7 +190,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
|
||||
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
|
||||
@@ -270,3 +273,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
@@ -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;
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -11,7 +10,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||
use tokio::time::sleep;
|
||||
|
||||
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,7 +19,7 @@ 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();
|
||||
@@ -54,13 +54,13 @@ pub async fn install() -> Result<()> {
|
||||
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
|
||||
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.");
|
||||
@@ -198,7 +198,7 @@ impl Install for ADBUSBDevice {
|
||||
/// 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} ... ");
|
||||
print!("Sending file {dest} ... ");
|
||||
let file_name = Path::new(dest)
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -18,7 +18,8 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::InstallTpLink;
|
||||
use crate::util::{telnet_send_command, telnet_send_file};
|
||||
use crate::output::println;
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
|
||||
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
||||
|
||||
@@ -378,10 +379,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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
@@ -19,7 +18,8 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::WingtechArgs 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};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginResponse {
|
||||
@@ -89,11 +89,11 @@ pub async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Res
|
||||
}
|
||||
|
||||
async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||
echo!("Starting telnet ... ");
|
||||
print!("Starting telnet ... ");
|
||||
start_telnet(&admin_ip, &admin_password).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");
|
||||
@@ -149,7 +149,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
|
||||
telnet_send_command(addr, "shutdown -r -t 1 now", "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"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/lib.rs"
|
||||
bytes = "1.5.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
crc = "3.0.1"
|
||||
deku = { version = "0.18.0", features = ["logging"] }
|
||||
deku = { version = "0.20.0", features = ["logging"] }
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
nix = { version = "0.29.0", features = ["feature"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::hdlc::{self, hdlc_decapsulate};
|
||||
use log::{error, warn};
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||
@@ -131,7 +131,7 @@ pub enum Message {
|
||||
log_type: u16,
|
||||
timestamp: Timestamp,
|
||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
|
||||
#[deku(ctx = "*log_type, *inner_length - 12")]
|
||||
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
|
||||
body: LogBody,
|
||||
},
|
||||
|
||||
@@ -141,10 +141,13 @@ pub enum Message {
|
||||
// pass those opcodes down to their respective parsers.
|
||||
#[deku(id_pat = "_")]
|
||||
Response {
|
||||
opcode: u32,
|
||||
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||
opcode2: u8,
|
||||
opcode3: u8,
|
||||
opcode4: u8,
|
||||
subopcode: u32,
|
||||
status: u32,
|
||||
#[deku(ctx = "*opcode, *subopcode")]
|
||||
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||
payload: ResponsePayload,
|
||||
},
|
||||
}
|
||||
@@ -189,20 +192,22 @@ pub enum LogBody {
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||
Nas4GMessage {
|
||||
#[deku(ctx = "log_type")]
|
||||
#[deku(skip, default = "log_type")]
|
||||
log_type: u16,
|
||||
#[deku(ctx = "*log_type")]
|
||||
direction: Nas4GMessageDirection,
|
||||
ext_header_version: u8,
|
||||
rrc_rel: u8,
|
||||
rrc_version_minor: u8,
|
||||
rrc_version_major: u8,
|
||||
// message length = hdr_len - (sizeof(ext_header_version) + sizeof(rrc_rel) + sizeof(rrc_version_minor) + sizeof(rrc_version_major))
|
||||
#[deku(count = "hdr_len - 4")]
|
||||
#[deku(count = "hdr_len.saturating_sub(4)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x11eb")]
|
||||
IpTraffic {
|
||||
// is this right?? based on https://github.com/P1sec/QCSuper/blob/81dbaeee15ec7747e899daa8e3495e27cdcc1264/src/modules/pcap_dump.py#L378
|
||||
#[deku(count = "hdr_len - 8")]
|
||||
#[deku(count = "hdr_len.saturating_sub(8)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x713a")]
|
||||
@@ -613,4 +618,83 @@ mod test {
|
||||
Err(DiagParsingError::HdlcDecapsulationError(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_inner_length_underflow() {
|
||||
// Regression test: inner_length < 12 previously caused panic.
|
||||
// Fixed by using saturating_sub in Message::Log body length calculation.
|
||||
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
let _ = Message::from_bytes((fuzz_data, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_nas_hdr_len_underflow() {
|
||||
// Regression test for two things:
|
||||
// - hdr_len < 4 previously caused panic in Nas4GMessage.
|
||||
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
|
||||
let nas_msg =
|
||||
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Log {
|
||||
log_type: 0xb0e2,
|
||||
body: LogBody::Nas4GMessage {
|
||||
direction: Nas4GMessageDirection::Downlink,
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
|
||||
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
|
||||
// Fixed by using saturating_sub for msg length calculation.
|
||||
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
|
||||
let _ = Message::from_bytes((ip_msg, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_response_opcode_parsing() {
|
||||
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
|
||||
// The issue was that deku 0.20 requires an `id` field for `id_pat = "_"` variants,
|
||||
// but in deku 0.18 the discriminant was NOT consumed from the stream.
|
||||
// This caused a 1-byte offset, making opcode and all subsequent fields misaligned.
|
||||
// Fixed by splitting the opcode into 4 separate u8 fields so the discriminant byte
|
||||
// becomes the first byte of the opcode, matching the old deku 0.18 behavior.
|
||||
let response_msg = b"\x73\x00\x00\x00\x03\x00\x00\x00\x0a\x00\xec\xb0\x8e\x51\x02\x6f\x2a\xc5\x0b\x01\x01\x09\x05\x00\x07\x45\x8e\x14\x7d";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((response_msg, 0)).unwrap();
|
||||
|
||||
// Verify the opcode is correctly parsed as 115 (0x73 in first byte)
|
||||
// In little-endian: [0x73, 0x00, 0x00, 0x00] = 0x00000073 = 115
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Response {
|
||||
opcode1: 0x73,
|
||||
opcode2: 0x00,
|
||||
opcode3: 0x00,
|
||||
opcode4: 0x00,
|
||||
subopcode: 3,
|
||||
status: 2968256522, // [0x0a, 0x00, 0xec, 0xb0] in LE
|
||||
payload: ResponsePayload::LogConfig(LogConfigResponse::SetMask),
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
|
||||
// Verify we consumed the expected number of bytes
|
||||
assert_eq!(rest.len(), 17);
|
||||
}
|
||||
}
|
||||
|
||||
47
logo/combined.svg
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
|
||||
<defs>
|
||||
<style>
|
||||
.j {
|
||||
fill: #4e4eb1;
|
||||
}
|
||||
|
||||
.k {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.l {
|
||||
fill: #94ea18;
|
||||
}
|
||||
|
||||
.m {
|
||||
fill: #3f3da0;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="j" d="M1026.11,508.07v606.14c0,25.85-20.96,46.8-46.81,46.8H216.38c-25.85,0-46.81-20.95-46.81-46.8V508.07c0-25.85,20.96-46.81,46.81-46.81h24.34c.23-9.13.81-18.28,1.76-27.43,6.81-65.43,32.02-127.21,72.9-178.69,30.81-38.81,69.55-70.56,113.35-93.12-1.1-9.47-3.19-19.3-6.2-29.01-4.19-13.51-16.95-54.61,7.94-88.38,14.02-19.04,36.54-29.97,61.78-29.97,11.78,0,22.24,2.31,28.55,3.71l1.06.23c41.19,9.1,82.37,32.17,122.41,68.55,22.09,20.08,39.04,39.98,49.6,53.49,53.65,17.44,102.38,47.9,141.81,88.77,44.44,46.07,75.21,103.3,88.97,165.51l.03.16c1.21,5.53,6.3,31.51,4.09,66.18h50.53c25.85,0,46.81,20.96,46.81,46.81Z"/>
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="l" points="911.94 512.54 911.94 885.6 817.63 885.6 817.63 622.92 850.38 640.91 848.07 599.67 881.89 602.46 867.06 568.16 897.65 563.99 873.55 534.32 894.99 512.54 911.94 512.54"/>
|
||||
<polygon class="l" points="798.22 627.73 798.22 885.6 703.91 885.6 703.91 605.81 735.88 605.81 734.99 628.87 764.18 617.75 772.98 652.97 798.22 627.73"/>
|
||||
<rect class="l" x="590.19" y="699.08" width="94.3" height="186.53"/>
|
||||
<rect class="l" x="476.47" y="792.34" width="94.3" height="93.26"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="k" d="M866.62,471.66c-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-10.5,7.2-26.5,9.63-42.59,5.55-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-27.8,6.2-34.19-28.79-30.92-54.86-5.27,4.84-11.11,9.07-18.06,13.42-20.08,12.53-58.23,19.42-71.27,7.28-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,10.39-.79,88.74-4.15,117.38,53.06,19.89,39.73-10.47,28.34-18.03,27.74-24.39-1.9-41.51,7.12-53.05,8.96-6.01.96-21.83,2.59-36-3.19,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,2.24,7.25,19.97,34.39-23.13,23.66-66.88-16.62-77.78-104.86-77.6-104.18-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-14.69-47.31,12.2-40.45,28.08-36.95,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61Z"/>
|
||||
<path class="l" d="M492.25,75.5c5.5,0,11.52,1.42,16.47,2.51,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-7.05,4.84-16.6,7.52-26.98,7.52-5.07,0-10.33-.64-15.61-1.98-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-2.2.49-4.26.72-6.2.72-22.56,0-27.73-31.57-24.72-55.59-5.27,4.84-11.11,9.07-18.06,13.42-12.73,7.94-32.72,13.62-48.97,13.62-9.39,0-17.53-1.89-22.3-6.34-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,1.66-.13,5.05-.32,9.69-.32,24.44,0,83.62,5.3,107.69,53.38,12.44,24.85,5.22,29.7-3.78,29.7-5.39,0-11.42-1.74-14.25-1.96-2.3-.18-4.54-.26-6.71-.26-20.86,0-35.89,7.55-46.34,9.22-2.63.42-7.14.97-12.56.97-6.98,0-15.47-.91-23.44-4.16,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,1.88,6.08,14.65,26.14-6.68,26.14-4.11,0-9.49-.74-16.45-2.48-66.62-16.56-77.7-104.19-77.61-104.19,0,0,0,0,0,0-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-10.11-32.56-.52-39.46,11.61-39.46M492.25,64.66h0c-11.93,0-18.3,5.24-21.53,9.63-6.75,9.16-6.89,23.11-.44,43.89,8.06,25.96,11.01,52.68,8.46,76.01-48.7,19.18-91.54,50.9-124.21,92.05-34.95,44.03-56.5,96.85-62.31,152.76-7.45,71.69,11.28,143.18,52.77,201.77-.3,6.8-.66,20.57.77,29.29h.03c.55,5.23,2.62,13.77,2.9,14.94,2.01,8.19,5.68,20.72,11.79,33.91,15.93,34.37,40.48,56.54,70.98,64.12,7.57,1.88,13.8,2.8,19.07,2.8,9.38,0,16.11-3.01,20-8.94,6.36-9.71,1.24-21.49-1.82-28.54-.42-.96-.99-2.28-1.13-2.72-2.3-7.49-3.39-15.04-3.44-23.74-.01-4.93.31-9.46.63-13.83.35-4.85.68-9.43.42-13.62-.24-3.82-.98-10.84-3.23-18.83,1.6.09,3.23.13,4.89.13,6.1,0,11.13-.6,14.28-1.11,4.12-.66,8.5-1.98,13.14-3.37,8.91-2.68,19.02-5.72,31.48-5.72,1.95,0,3.92.08,5.87.23.44.04,1.85.31,2.88.51,3.25.63,7.71,1.48,12.21,1.48,10.62,0,15.86-4.87,18.38-8.95,5.1-8.26,3.59-19.5-4.9-36.46-9.61-19.2-24.88-33.99-45.48-44.11,16.72-1.18,35.52-6.93,48.52-15.04.38-.24.75-.47,1.12-.71,1.15,12.72,4.79,25.3,12.32,33.84,6.16,6.98,14.32,10.68,23.59,10.68,2.75,0,5.63-.33,8.56-.98l.16-.04.16-.04c22.76-5.79,43.36-25.23,54.24-42.53,4.05,2.04,8.12,3.95,12.18,5.7,10.94,4.76,22.38,8.64,33.99,11.51,8.64,2.15,19.07,3.94,29.01,4.99,6.94,4.66,16.46,10,30.12,13.47,6.05,1.53,12.2,2.31,18.28,2.31,12.5,0,24.26-3.35,33.12-9.43,12.99-8.92,17.77-24.36,20.62-33.59.12-.38.23-.75.34-1.1.55-.9,1.93-2.85,3.07-4.45,9.02-12.73,27.78-39.21,33.27-79.73,4.92-36.33-.86-64.32-1.54-67.4-11.77-53.21-38.1-102.18-76.14-141.6-37.37-38.74-84.33-66.44-136.03-80.3-10.43-15.06-29.59-38.38-53.01-59.67-33.5-30.44-67-49.53-99.58-56.73l-.97-.21c-5.13-1.14-11.51-2.55-17.84-2.55h0ZM442.69,562.99c-.4-16.55,7.59-32.52,22.37-44,3-2.34,6.2-4.5,9.57-6.47-1.73,5.23-3.12,10.59-4.03,15.79-2.87,16.33-1.07,29.19,5.2,37.56-12.68-2.26-24.23-2.87-33-2.87h-.1Z"/>
|
||||
</g>
|
||||
<path class="m" d="M865.24,408.05c-23-103.97-103.18-188.19-208.27-214.77-16.45-24.98-77.12-99.56-148.24-115.27-15.88-3.5-42.77-10.36-28.08,36.95,9.78,31.5,11.84,62.43,8,87.06-99.04,36.49-174.06,126.71-185.64,238.1-7.61,73.2,13.06,142.53,53.01,197.41v.04s-1.25,20.35.45,30.74c-.18-.68,10.73,87.56,77.6,104.18,43.11,10.73,25.37-16.41,23.13-23.66-3.02-9.86-3.88-18.88-3.92-26.89-.03-11,1.49-20.08,1.07-26.84-.38-6.05-2.24-21.83-10.96-34.38,14.17,5.78,29.99,4.15,36,3.19,11.54-1.84,28.65-10.86,53.05-8.96,7.57.6,37.93,11.99,18.03-27.74-28.64-57.21-106.99-53.85-117.38-53.06-4.92-23.68,4.34-47.46,25.3-63.73,11.53-8.96,24.67-15.02,38.55-18.89-.54.75-1.07,1.52-1.59,2.31-12.68,18.81-21.85,55.64-9.17,67.45,13.04,12.14,51.19,5.25,71.27-7.28,6.96-4.35,12.79-8.58,18.06-13.42-3.27,26.08,3.12,61.06,30.92,54.86,19.46-4.95,38.54-22.36,48.64-39.27,1.4-2.44,2.66-4.91,3.81-7.43,6.79,3.84,13.79,7.34,20.95,10.43,10.44,4.54,21.25,8.2,32.3,10.94,7.69,1.91,18.67,3.93,30.25,5,6.64,4.68,15.59,10.09,28.94,13.48,16.1,4.08,32.1,1.65,42.59-5.55,10.64-7.31,14.47-21.78,16.95-29.64,1.79-5.6,29.17-33.26,35.73-81.74,4.61-34.02-.64-60.22-1.38-63.61ZM698.62,535.21c.31-.04.62.09.77.37,4.68,7.86,11.96,14.67,19.01,21.8h0c-.58.01-21.3-8.58-39.01-23,6.13,1.27,12.39,1.79,19.23.83ZM671.77,527.49c-1.13-.46-1.3-.88-1.14-2.66.77-9.07,5.82-16.16,10.93-20.53.31-.26.81-.16.98.22,2.4,5.71,6.98,15.92,12.96,24.92.26.39.56.94-.42,1.04-8.52.98-18.84-1.18-23.31-3ZM691.99,476.52c4.83-32.33-15.17-44.17-15.17-44.17,0,0,22.94,5.93,25.68,24.26,2.75,18.33-10.51,19.91-10.51,19.91ZM677.71,497.73c.14-.01.2.12.11.22-4.65,4.56-9.67,11.35-13.41,20.9-.11.28-.46.37-.68.15-6.44-6.64-11.95-13.41-18.33-23.85-.11-.18.05-.41.26-.34,8.24,3.08,20.25,4.56,32.06,2.93ZM675.5,492.17c-12.08,1.41-29.28-1.93-32.72-3.84-.31-.18-.47-.54-.34-.85,2.81-7.23,14.05-17.91,23.85-21.82.52-.22,1.1.08,1.27.62,2.17,7.54,5.02,15.8,8.69,24.53.24.6-.12,1.27-.76,1.36ZM638.13,435.55c3.85-3.63,9.61-7.69,15.87-9.06,2.77-.61,5.45,1.25,5.76,4.05.64,5.98,1.45,12.83,3.65,22.74.04.15-.12.28-.26.23-11.11-3.76-19.5-9.49-25.1-17.11-.19-.27-.15-.64.08-.85ZM634.04,441.3c.12-.42.69-.5.92-.12,5.84,8.96,13.9,14.63,27.01,18.13.39.11.46.62.11.83-12.29,6.77-18.43,14.71-22.05,21.26-.15.24-.5.26-.64.01-6.03-10.09-8.48-28.11-5.36-40.11ZM504.85,481.07c-9.97,2.25-19.72,5.46-29.01,9.83-19.57,8.95-36.31,24.18-44.68,42.96-.24-3.48-.81-7.35-1.91-11.89-5.61-23.38-26.97-21.76-36.75-20.94-14.6,1.25-37.85,9.15-46.35-9.32-13.25-28.71,23.42-67.77,66.21-67.81,45.71-.04,70.58,37.07,74.57,43.54.3.5.98.58,1.42.19,17.52-15.34,42.4-13.11,42.4-13.11,0,0-12.73,10.33-25.91,26.55ZM506.46,325.88c-19.31,5.89-45.62,9.34-64.51-7.3-18.89-16.64-23.28-83.25,44.75-108.21,0,0-11.8,59.45,17.25,63,24.54,3,63.04-4.92,90.77,14.69,27.16,19.2,20.33,37.22-4.8,31.99-28.82-6.01-64.15-.07-83.46,5.83ZM650.12,553.16c-9.59,14.7-28,31.13-45.2,35.36-4.85,1.04-9.6.9-13.36-2.52-9.21-8.8-10.79-25.85-10.73-38.27.05-4.56.39-9.18,1.18-13.72,6.1-6.79,11.74-15,18.09-25.99,7.28,4.73,15.57,11.5,21.67,16.06,10.37,7.85,21.14,15.31,32.4,21.94-1.23,2.43-2.58,4.81-4.05,7.15ZM713.7,563.98c-18.41-4.71-35.76-12.35-52.07-21.86-1.49-.85-2.96-1.75-4.43-2.64-10.69-6.51-20.94-13.76-30.78-21.49-6.58-4.98-14.75-11.91-22.37-16.95,6.97-12.95,20.87-53.18,20.87-53.18,0,0-2.78,38.82,37.13,77.75,1.14,1.13,2.35,2.26,3.57,3.38.35.33.69.64,1.04.96,15.3,13.78,40.15,28.64,62.76,36.68.8.28,1.61.66,2.47,1.14-6.13-.95-12.19-2.2-18.17-3.78ZM766.42,461.91c-8.54,13.07-27.43,11.95-46.04-12.9-2.79-3.73-7.95-15.96-31.78-35.34-23.5-19.09-29.76-47.18-11.05-64.82,16.35-15.42,52.13-7.54,74.2,21.63,24.75,32.69,27.8,71.3,14.67,91.42Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="l" d="M250.23,965.33l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
|
||||
<polygon class="l" points="373.09 993.66 373.09 1078.67 316.42 1078.67 288.08 1050.33 288.08 1021.81 316.23 993.66 316.42 993.66 344.76 1022 316.42 1022 316.42 1050.33 344.76 1050.33 344.76 993.66 316.42 993.66 288.08 965.33 344.76 965.33 373.09 993.66"/>
|
||||
<path class="l" d="M439.29,1050.33h-28.34v-85.01h-28.34v85.01l28.34,28.34h28.34v28.34l28.34,28.34v-56.67l-28.34-28.34ZM439.29,965.33v85.01h28.34v-56.67l-28.34-28.34Z"/>
|
||||
<path class="k" d="M533.82,965.33h-28.34v-56.67h-28.34v56.67l28.34,28.34h28.34v85.01h28.34v-85.01l-28.34-28.34ZM477.14,993.66v56.67l28.34,28.34v-85.01h-28.34Z"/>
|
||||
<path class="k" d="M600.01,1050.33v-85.01h-28.34v85.01l28.34,28.34h28.34v-28.34h-28.34ZM628.35,965.33v85.01l28.34,28.34v-113.35h-28.34Z"/>
|
||||
<path class="k" d="M666.2,965.33v113.35h28.34v-85.01l-28.34-28.34ZM722.88,965.33h-28.34v28.34h28.34v56.67l28.34,28.34v-85.01l-28.34-28.34Z"/>
|
||||
<path class="k" d="M789.07,965.33v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34ZM789.07,1050.33v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34Z"/>
|
||||
<path class="k" d="M911.94,993.66l-28.34-28.34h-56.67v85.01l28.34,28.34h56.67l-28.34-28.34h-28.34v-56.67h28.34v28.34h-28.34l28.34,28.34h.1l-.19-.19,28.42-28.4v-28.09Z"/>
|
||||
<path class="k" d="M949.77,965.33l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
29
logo/orca.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 977.04 977.04">
|
||||
<defs>
|
||||
<style>
|
||||
.j {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.k {
|
||||
fill: #94ea18;
|
||||
}
|
||||
|
||||
.l {
|
||||
fill: #3f3da0;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<polygon class="k" points="764.99 525.93 764.99 898.99 670.69 898.99 670.69 636.31 703.43 654.3 701.12 613.06 734.95 615.85 720.12 581.55 750.71 577.38 726.61 547.71 748.05 525.93 764.99 525.93"/>
|
||||
<polygon class="k" points="651.28 641.12 651.28 898.99 556.97 898.99 556.97 619.2 588.93 619.2 588.05 642.26 617.24 631.13 626.04 666.36 651.28 641.12"/>
|
||||
<rect class="k" x="443.25" y="712.47" width="94.3" height="186.53"/>
|
||||
<rect class="k" x="329.53" y="805.73" width="94.3" height="93.26"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="j" d="M719.68,485.05c-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-10.5,7.2-26.5,9.63-42.59,5.55-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-27.8,6.2-34.19-28.79-30.92-54.86-5.27,4.84-11.11,9.07-18.06,13.42-20.08,12.53-58.23,19.42-71.27,7.28-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,10.39-.79,88.74-4.15,117.38,53.06,19.89,39.73-10.47,28.34-18.03,27.74-24.39-1.9-41.51,7.12-53.05,8.96-6.01.96-21.83,2.59-36-3.19,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,2.24,7.25,19.97,34.39-23.13,23.66-66.88-16.62-77.78-104.86-77.6-104.18-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-14.69-47.31,12.2-40.45,28.08-36.95,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61Z"/>
|
||||
<path class="k" d="M345.31,88.89c5.5,0,11.52,1.42,16.47,2.51,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-7.05,4.84-16.6,7.52-26.98,7.52-5.07,0-10.33-.64-15.61-1.98-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-2.2.49-4.26.72-6.2.72-22.56,0-27.73-31.57-24.72-55.59-5.27,4.84-11.11,9.07-18.06,13.42-12.73,7.94-32.72,13.62-48.97,13.62-9.39,0-17.53-1.89-22.3-6.34-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,1.66-.13,5.05-.32,9.69-.32,24.44,0,83.62,5.3,107.69,53.38,12.44,24.85,5.22,29.7-3.78,29.7-5.39,0-11.42-1.74-14.25-1.96-2.3-.18-4.54-.26-6.71-.26-20.86,0-35.89,7.55-46.34,9.22-2.63.42-7.14.97-12.56.97-6.98,0-15.47-.91-23.44-4.16,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,1.88,6.08,14.65,26.14-6.68,26.14-4.11,0-9.49-.74-16.45-2.48-66.62-16.56-77.7-104.19-77.61-104.19,0,0,0,0,0,0-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-10.11-32.56-.52-39.46,11.61-39.46M345.31,78.04h0c-11.93,0-18.3,5.24-21.53,9.63-6.75,9.16-6.89,23.11-.44,43.89,8.06,25.96,11.01,52.68,8.46,76.01-48.7,19.18-91.54,50.9-124.21,92.05-34.95,44.03-56.5,96.85-62.31,152.76-7.45,71.69,11.28,143.18,52.77,201.77-.3,6.8-.66,20.57.77,29.29h.03c.55,5.23,2.62,13.77,2.9,14.94,2.01,8.19,5.68,20.72,11.79,33.91,15.93,34.37,40.48,56.54,70.98,64.12,7.57,1.88,13.8,2.8,19.07,2.8,9.38,0,16.11-3.01,20-8.94,6.36-9.71,1.24-21.49-1.82-28.54-.42-.96-.99-2.28-1.13-2.72-2.3-7.49-3.39-15.04-3.44-23.74-.01-4.93.31-9.46.63-13.83.35-4.85.68-9.43.42-13.62-.24-3.82-.98-10.84-3.23-18.83,1.6.09,3.23.13,4.89.13,6.1,0,11.13-.6,14.28-1.11,4.12-.66,8.5-1.98,13.14-3.37,8.91-2.68,19.02-5.72,31.48-5.72,1.95,0,3.92.08,5.87.23.44.04,1.85.31,2.88.51,3.25.63,7.71,1.48,12.21,1.48,10.62,0,15.86-4.87,18.38-8.95,5.1-8.26,3.59-19.5-4.9-36.46-9.61-19.2-24.88-33.99-45.48-44.11,16.72-1.18,35.52-6.93,48.52-15.04.38-.24.75-.47,1.12-.71,1.15,12.72,4.79,25.3,12.32,33.84,6.16,6.98,14.32,10.68,23.59,10.68,2.75,0,5.63-.33,8.56-.98l.16-.04.16-.04c22.76-5.79,43.36-25.23,54.24-42.53,4.05,2.04,8.12,3.95,12.18,5.7,10.94,4.76,22.38,8.64,33.99,11.51,8.64,2.15,19.07,3.94,29.01,4.99,6.94,4.66,16.46,10,30.12,13.47,6.05,1.53,12.2,2.31,18.28,2.31,12.5,0,24.26-3.35,33.12-9.43,12.99-8.92,17.77-24.36,20.62-33.59.12-.38.23-.75.34-1.1.55-.9,1.93-2.85,3.07-4.45,9.02-12.73,27.78-39.21,33.27-79.73,4.92-36.33-.86-64.32-1.54-67.4-11.77-53.21-38.1-102.18-76.14-141.6-37.37-38.74-84.33-66.44-136.03-80.3-10.43-15.06-29.59-38.38-53.01-59.67-33.5-30.44-67-49.53-99.58-56.73l-.97-.21c-5.13-1.14-11.51-2.55-17.84-2.55h0ZM295.75,576.37c-.4-16.55,7.59-32.52,22.37-44,3-2.34,6.2-4.5,9.57-6.47-1.73,5.23-3.12,10.59-4.03,15.79-2.87,16.33-1.07,29.19,5.2,37.56-12.68-2.26-24.23-2.87-33-2.87h-.1Z"/>
|
||||
</g>
|
||||
<path class="l" d="M718.29,421.44c-23-103.97-103.18-188.19-208.27-214.77-16.45-24.98-77.12-99.56-148.24-115.27-15.88-3.5-42.77-10.36-28.08,36.95,9.78,31.5,11.84,62.43,8,87.06-99.04,36.49-174.06,126.71-185.64,238.1-7.61,73.2,13.06,142.53,53.01,197.41v.04s-1.25,20.35.45,30.74c-.18-.68,10.73,87.56,77.6,104.18,43.11,10.73,25.37-16.41,23.13-23.66-3.02-9.86-3.88-18.88-3.92-26.89-.03-11,1.49-20.08,1.07-26.84-.38-6.05-2.24-21.83-10.96-34.38,14.17,5.78,29.99,4.15,36,3.19,11.54-1.84,28.65-10.86,53.05-8.96,7.57.6,37.93,11.99,18.03-27.74-28.64-57.21-106.99-53.85-117.38-53.06-4.92-23.68,4.34-47.46,25.3-63.73,11.53-8.96,24.67-15.02,38.55-18.89-.54.75-1.07,1.52-1.59,2.31-12.68,18.81-21.85,55.64-9.17,67.45,13.04,12.14,51.19,5.25,71.27-7.28,6.96-4.35,12.79-8.58,18.06-13.42-3.27,26.08,3.12,61.06,30.92,54.86,19.46-4.95,38.54-22.36,48.64-39.27,1.4-2.44,2.66-4.91,3.81-7.43,6.79,3.84,13.79,7.34,20.95,10.43,10.44,4.54,21.25,8.2,32.3,10.94,7.69,1.91,18.67,3.93,30.25,5,6.64,4.68,15.59,10.09,28.94,13.48,16.1,4.08,32.1,1.65,42.59-5.55,10.64-7.31,14.47-21.78,16.95-29.64,1.79-5.6,29.17-33.26,35.73-81.74,4.61-34.02-.64-60.22-1.38-63.61ZM551.68,548.59c.31-.04.62.09.77.37,4.68,7.86,11.96,14.67,19.01,21.8h0c-.58.01-21.3-8.58-39.01-23,6.13,1.27,12.39,1.79,19.23.83ZM524.83,540.88c-1.13-.46-1.3-.88-1.14-2.66.77-9.07,5.82-16.16,10.93-20.53.31-.26.81-.16.98.22,2.4,5.71,6.98,15.92,12.96,24.92.26.39.56.94-.42,1.04-8.52.98-18.84-1.18-23.31-3ZM545.05,489.91c4.83-32.33-15.17-44.17-15.17-44.17,0,0,22.94,5.93,25.68,24.26,2.75,18.33-10.51,19.91-10.51,19.91ZM530.77,511.11c.14-.01.2.12.11.22-4.65,4.56-9.67,11.35-13.41,20.9-.11.28-.46.37-.68.15-6.44-6.64-11.95-13.41-18.33-23.85-.11-.18.05-.41.26-.34,8.24,3.08,20.25,4.56,32.06,2.93ZM528.56,505.56c-12.08,1.41-29.28-1.93-32.72-3.84-.31-.18-.47-.54-.34-.85,2.81-7.23,14.05-17.91,23.85-21.82.52-.22,1.1.08,1.27.62,2.17,7.54,5.02,15.8,8.69,24.53.24.6-.12,1.27-.76,1.36ZM491.19,448.94c3.85-3.63,9.61-7.69,15.87-9.06,2.77-.61,5.45,1.25,5.76,4.05.64,5.98,1.45,12.83,3.65,22.74.04.15-.12.28-.26.23-11.11-3.76-19.5-9.49-25.1-17.11-.19-.27-.15-.64.08-.85ZM487.09,454.69c.12-.42.69-.5.92-.12,5.84,8.96,13.9,14.63,27.01,18.13.39.11.46.62.11.83-12.29,6.77-18.43,14.71-22.05,21.26-.15.24-.5.26-.64.01-6.03-10.09-8.48-28.11-5.36-40.11ZM357.91,494.46c-9.97,2.25-19.72,5.46-29.01,9.83-19.57,8.95-36.31,24.18-44.68,42.96-.24-3.48-.81-7.35-1.91-11.89-5.61-23.38-26.97-21.76-36.75-20.94-14.6,1.25-37.85,9.15-46.35-9.32-13.25-28.71,23.42-67.77,66.21-67.81,45.71-.04,70.58,37.07,74.57,43.54.3.5.98.58,1.42.19,17.52-15.34,42.4-13.11,42.4-13.11,0,0-12.73,10.33-25.91,26.55ZM359.52,339.27c-19.31,5.89-45.62,9.34-64.51-7.3-18.89-16.64-23.28-83.25,44.75-108.21,0,0-11.8,59.45,17.25,63,24.54,3,63.04-4.92,90.77,14.69,27.16,19.2,20.33,37.22-4.8,31.99-28.82-6.01-64.15-.07-83.46,5.83ZM503.17,566.55c-9.59,14.7-28,31.13-45.2,35.36-4.85,1.04-9.6.9-13.36-2.52-9.21-8.8-10.79-25.85-10.73-38.27.05-4.56.39-9.18,1.18-13.72,6.1-6.79,11.74-15,18.09-25.99,7.28,4.73,15.57,11.5,21.67,16.06,10.37,7.85,21.14,15.31,32.4,21.94-1.23,2.43-2.58,4.81-4.05,7.15ZM566.76,577.37c-18.41-4.71-35.76-12.35-52.07-21.86-1.49-.85-2.96-1.75-4.43-2.64-10.69-6.51-20.94-13.76-30.78-21.49-6.58-4.98-14.75-11.91-22.37-16.95,6.97-12.95,20.87-53.18,20.87-53.18,0,0-2.78,38.82,37.13,77.75,1.14,1.13,2.35,2.26,3.57,3.38.35.33.69.64,1.04.96,15.3,13.78,40.15,28.64,62.76,36.68.8.28,1.61.66,2.47,1.14-6.13-.95-12.19-2.2-18.17-3.78ZM619.48,475.3c-8.54,13.07-27.43,11.95-46.04-12.9-2.79-3.73-7.95-15.96-31.78-35.34-23.5-19.09-29.76-47.18-11.05-64.82,16.35-15.42,52.13-7.54,74.2,21.63,24.75,32.69,27.8,71.3,14.67,91.42Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
23
logo/text.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1060.73 497.18">
|
||||
<defs>
|
||||
<style>
|
||||
.j {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.k {
|
||||
fill: #94ea18;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="k" d="M187.15,204.67l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
|
||||
<polygon class="k" points="310.01 233.01 310.01 318.02 253.34 318.02 225 289.68 225 261.16 253.15 233.01 253.34 233.01 281.68 261.35 253.34 261.35 253.34 289.68 281.68 289.68 281.68 233.01 253.34 233.01 225 204.67 281.68 204.67 310.01 233.01"/>
|
||||
<path class="k" d="M376.21,289.68h-28.34v-85.01h-28.34v85.01l28.34,28.34h28.34v28.34l28.34,28.34v-56.67l-28.34-28.34ZM376.21,204.67v85.01h28.34v-56.67l-28.34-28.34Z"/>
|
||||
<path class="j" d="M470.74,204.67h-28.34v-56.67h-28.34v56.67l28.34,28.34h28.34v85.01h28.34v-85.01l-28.34-28.34ZM414.06,233.01v56.67l28.34,28.34v-85.01h-28.34Z"/>
|
||||
<path class="j" d="M536.93,289.68v-85.01h-28.34v85.01l28.34,28.34h28.34v-28.34h-28.34ZM565.27,204.67v85.01l28.34,28.34v-113.35h-28.34Z"/>
|
||||
<path class="j" d="M603.12,204.67v113.35h28.34v-85.01l-28.34-28.34ZM659.8,204.67h-28.34v28.34h28.34v56.67l28.34,28.34v-85.01l-28.34-28.34Z"/>
|
||||
<path class="j" d="M725.99,204.67v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34ZM725.99,289.68v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34Z"/>
|
||||
<path class="j" d="M848.86,233.01l-28.34-28.34h-56.67v85.01l28.34,28.34h56.67l-28.34-28.34h-28.34v-56.67h28.34v28.34h-28.34l28.34,28.34h.1l-.19-.19,28.42-28.4v-28.09Z"/>
|
||||
<path class="j" d="M886.69,204.67l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||