Compare commits
73 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 | |||
| e162070a04 | |||
| 2e42750b09 | |||
| e375e4587a | |||
| 2a30e2d709 | |||
| fe2b8b3456 | |||
| cedfe2d4d7 | |||
| 22be337f62 | |||
| 6326c5e783 | |||
| ea5aa6cee2 | |||
| 65d4f22e09 | |||
| 450434b4f9 | |||
| 4e93e03e6a | |||
| e416d6e311 | |||
| 0eebe890c1 | |||
| 28c9f44f73 | |||
| 85fa73ddd6 | |||
| b8b90268b9 | |||
| 9e5de4a445 | |||
| 643fb802be | |||
| 93f22172cc | |||
| d5f2dd9813 |
@@ -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",
|
||||||
|
]
|
||||||
@@ -2,6 +2,12 @@ name: Bug Report
|
|||||||
description: File a bug report.
|
description: File a bug report.
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Bug Report Details
|
label: Bug Report Details
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Rayhunter Mattermost
|
- name: Questions and community
|
||||||
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
|
url: https://efforg.github.io/rayhunter/support-feedback-community.html
|
||||||
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
|
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help on GitHub discussions or Mattermost
|
||||||
- name: Rayhunter Security Policy
|
- name: Rayhunter Security Policy
|
||||||
url: https://github.com/EFForg/rayhunter/security/advisories/new
|
url: https://github.com/EFForg/rayhunter/security/advisories/new
|
||||||
about: Please report security vulnerabilities here.
|
about: Please report security vulnerabilities here.
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ name: Feature Request
|
|||||||
description: Suggest a new feature or improvement to Rayhunter
|
description: Suggest a new feature or improvement to Rayhunter
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ name: Installer Issue
|
|||||||
description: File an bug related to an installer issue.
|
description: File an bug related to an installer issue.
|
||||||
labels: ["bug", "installer"]
|
labels: ["bug", "installer"]
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Rayhunter Version
|
label: Rayhunter Version
|
||||||
|
|||||||
@@ -25,18 +25,20 @@ jobs:
|
|||||||
web_changed: ${{ steps.files_changed.outputs.web_count }}
|
web_changed: ${{ steps.files_changed.outputs.web_count }}
|
||||||
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
||||||
installer_changed: ${{ steps.files_changed.outputs.installer_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 }}
|
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
- name: detect file changes
|
- name: detect file changes
|
||||||
id: files_changed
|
id: files_changed
|
||||||
run: |
|
run: |
|
||||||
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
|
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 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
|
then
|
||||||
echo "building everything"
|
echo "building everything"
|
||||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
echo code_count=forced >> "$GITHUB_OUTPUT"
|
||||||
@@ -44,13 +46,15 @@ jobs:
|
|||||||
echo web_count=forced >> "$GITHUB_OUTPUT"
|
echo web_count=forced >> "$GITHUB_OUTPUT"
|
||||||
echo docs_count=forced >> "$GITHUB_OUTPUT"
|
echo docs_count=forced >> "$GITHUB_OUTPUT"
|
||||||
echo installer_count=forced >> "$GITHUB_OUTPUT"
|
echo installer_count=forced >> "$GITHUB_OUTPUT"
|
||||||
|
echo installer_gui_count=forced >> "$GITHUB_OUTPUT"
|
||||||
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
|
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
|
||||||
else
|
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 "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 "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 "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 "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"
|
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -63,6 +67,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install mdBook
|
- name: Install mdBook
|
||||||
run: |
|
run: |
|
||||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||||
@@ -80,6 +86,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install mdBook
|
- name: Install mdBook
|
||||||
run: |
|
run: |
|
||||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||||
@@ -104,6 +112,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
@@ -124,7 +134,35 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
NO_FIRMWARE_BIN=true cargo clippy --verbose
|
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
|
needs: files_changed
|
||||||
if: needs.files_changed.outputs.web_changed != '0'
|
if: needs.files_changed.outputs.web_changed != '0'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -135,11 +173,30 @@ jobs:
|
|||||||
working-directory: daemon/web
|
working-directory: daemon/web
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run check
|
- run: npm run check
|
||||||
- run: npm run test
|
- 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:
|
windows_installer_check_and_test:
|
||||||
needs: files_changed
|
needs: files_changed
|
||||||
if: needs.files_changed.outputs.installer_changed != '0'
|
if: needs.files_changed.outputs.installer_changed != '0'
|
||||||
@@ -148,6 +205,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: cargo check
|
- name: cargo check
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -192,6 +251,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
@@ -205,7 +266,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_rootshell:
|
build_rootshell:
|
||||||
if: needs.files_changed.outputs.rootshell_changed != '0'
|
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
|
||||||
needs:
|
needs:
|
||||||
- check_and_test
|
- check_and_test
|
||||||
- files_changed
|
- files_changed
|
||||||
@@ -214,6 +275,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-musleabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
@@ -240,6 +303,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-musleabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
@@ -301,6 +366,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
@@ -313,6 +380,145 @@ jobs:
|
|||||||
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||||
if-no-files-found: error
|
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:
|
build_release_zip:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -334,6 +540,8 @@ jobs:
|
|||||||
- windows-x86_64
|
- windows-x86_64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Fix executable permissions on binaries
|
- name: Fix executable permissions on binaries
|
||||||
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
|
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
|
||||||
@@ -343,7 +551,7 @@ jobs:
|
|||||||
- name: Setup versioned release directory
|
- name: Setup versioned release directory
|
||||||
run: |
|
run: |
|
||||||
platform="${{ matrix.platform }}"
|
platform="${{ matrix.platform }}"
|
||||||
dest="rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}"
|
dest="rayhunter-v${VERSION}-${{ matrix.platform }}"
|
||||||
mkdir "$dest"
|
mkdir "$dest"
|
||||||
# Handle installer with proper extension for Windows
|
# Handle installer with proper extension for Windows
|
||||||
if [ "$platform" = "windows-x86_64" ]; then
|
if [ "$platform" = "windows-x86_64" ]; then
|
||||||
@@ -351,7 +559,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
mv installer-$platform/installer "$dest"/installer
|
mv installer-$platform/installer "$dest"/installer
|
||||||
fi
|
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"
|
zip -r "$dest.zip" "$dest"
|
||||||
sha256sum "$dest.zip" > "$dest.zip.sha256"
|
sha256sum "$dest.zip" > "$dest.zip.sha256"
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Ensure all Cargo.toml files have the same version defined.
|
- name: Ensure all Cargo.toml files have the same version defined.
|
||||||
run: |
|
run: |
|
||||||
defined_versions=$(find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
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 rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
find lib check daemon installer installer-gui rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
||||||
echo number of defined versions = $defined_versions
|
echo number of defined versions = $defined_versions
|
||||||
if [ $defined_versions != "1" ]
|
if [ $defined_versions != "1" ]
|
||||||
then
|
then
|
||||||
@@ -41,6 +43,8 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Our issue tracker is [on GitHub](https://github.com/EFForg/rayhunter/issues).
|
|||||||
|
|
||||||
- Issues should be actionable. If you don't have a
|
- Issues should be actionable. If you don't have a
|
||||||
specific feature request or bug report, consider [creating a
|
specific feature request or bug report, consider [creating a
|
||||||
discussion](https://github.com/EFForg/rayhunter/discussions) instead.
|
discussion](https://github.com/EFForg/rayhunter/discussions) or [joining our Mattermost](https://efforg.github.io/rayhunter/support-feedback-community.html) instead.
|
||||||
|
|
||||||
Example of a good bug report:
|
Example of a good bug report:
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,17 @@ members = [
|
|||||||
"rootshell",
|
"rootshell",
|
||||||
"telcom-parser",
|
"telcom-parser",
|
||||||
"installer",
|
"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"
|
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.
|
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.
|
→ Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-check"
|
name = "rayhunter-check"
|
||||||
version = "0.7.1"
|
version = "0.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ use walkdir::WalkDir;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long, help = "A file or directory of packet captures")]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
#[arg(short = 'P', long)]
|
#[arg(short = 'P', long, help = "Convert qmdl files to pcap before analysis")]
|
||||||
pcapify: bool,
|
pcapify: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long, help = "Show why some packets were skipped during analysis")]
|
||||||
show_skipped: bool,
|
show_skipped: bool,
|
||||||
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long, help = "Only print warnings/errors to stdout")]
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long, help = "Show debug messages")]
|
||||||
debug: bool,
|
debug: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.7.1"
|
version = "0.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88.0"
|
rust-version = "1.88.0"
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ pub fn update_ui(
|
|||||||
let display_level = config.ui_level;
|
let display_level = config.ui_level;
|
||||||
if display_level == 0 {
|
if display_level == 0 {
|
||||||
info!("Invisible mode, not spawning UI.");
|
info!("Invisible mode, not spawning UI.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let colorblind_mode = config.colorblind_mode;
|
let colorblind_mode = config.colorblind_mode;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^7.1.9",
|
"vite": "^7.1.11",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2760,9 +2760,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3033,9 +3033,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4737,9 +4737,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.9",
|
"version": "7.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^7.1.9",
|
"vite": "^7.1.11",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@
|
|||||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
<label for="test_analyzer" class="ml-2 block text-sm text-gray-700">
|
<label for="test_analyzer" class="ml-2 block text-sm text-gray-700">
|
||||||
Test Heuristic (noisey!)
|
Test Heuristic (noisy!)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
import { AnalysisManager } from '$lib/analysisManager.svelte';
|
import { AnalysisManager } from '$lib/analysisManager.svelte';
|
||||||
|
import { screenIsLgUp } from '$lib/stores/breakpoint';
|
||||||
import TableRow from './ManifestTableRow.svelte';
|
import TableRow from './ManifestTableRow.svelte';
|
||||||
import Card from './ManifestCard.svelte';
|
import Card from './ManifestCard.svelte';
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,27 +13,30 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!--For larger screens we use a table-->
|
<!--For larger screens we use a table-->
|
||||||
<table class="hidden table-auto text-left lg:table">
|
{#if $screenIsLgUp}
|
||||||
<thead>
|
<table class="table-auto text-left table">
|
||||||
<tr class="bg-gray-100 drop-shadow">
|
<thead>
|
||||||
<th class="p-2" scope="col">ID</th>
|
<tr class="bg-gray-100 drop-shadow">
|
||||||
<th class="p-2" scope="col">Started</th>
|
<th class="p-2" scope="col">ID</th>
|
||||||
<th class="p-2" scope="col">Last Message</th>
|
<th class="p-2" scope="col">Started</th>
|
||||||
<th class="p-2" scope="col">Size</th>
|
<th class="p-2" scope="col">Last Message</th>
|
||||||
<th class="p-2" scope="col">Download</th>
|
<th class="p-2" scope="col">Size</th>
|
||||||
<th class="p-2" scope="col">Analysis</th>
|
<th class="p-2" scope="col">Download</th>
|
||||||
<th class="p-2" scope="col"></th>
|
<th class="p-2" scope="col">Analysis</th>
|
||||||
</tr>
|
<th class="p-2" scope="col"></th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{#each entries as entry, i}
|
<tbody>
|
||||||
<TableRow {entry} current={false} {i} {manager} />
|
{#each entries as entry, i}
|
||||||
|
<TableRow {entry} current={false} {i} {manager} />
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{:else}
|
||||||
|
<!--For smaller screens we use cards-->
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#each entries as entry}
|
||||||
|
<Card {entry} current={false} {server_is_recording} {manager} />
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
{/if}
|
||||||
<!--For smaller screens we use cards-->
|
|
||||||
<div class="lg:hidden flex flex-col gap-4">
|
|
||||||
{#each entries as entry}
|
|
||||||
<Card {entry} current={false} {server_is_recording} {manager} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// stores/breakpoint.ts
|
||||||
|
import { readable, type Readable } from 'svelte/store';
|
||||||
|
import { breakpoints } from '../../theme';
|
||||||
|
|
||||||
|
type Breakpoint = keyof typeof breakpoints;
|
||||||
|
|
||||||
|
// Store that tracks if a specific breakpoint matches
|
||||||
|
export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean> {
|
||||||
|
return readable<boolean>(false, (set) => {
|
||||||
|
const width = breakpoints[breakpoint];
|
||||||
|
const mediaQuery = window.matchMedia(`(min-width: ${width})`);
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
|
set(mediaQuery.matches);
|
||||||
|
|
||||||
|
// Update on change
|
||||||
|
const handler = (e: MediaQueryListEvent) => set(e.matches);
|
||||||
|
mediaQuery.addEventListener('change', handler);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => mediaQuery.removeEventListener('change', handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create stores for each breakpoint
|
||||||
|
export const screenIsSmUp: Readable<boolean> = createBreakpointStore('sm');
|
||||||
|
export const screenIsMdUp: Readable<boolean> = createBreakpointStore('md');
|
||||||
|
export const screenIsLgUp: Readable<boolean> = createBreakpointStore('lg');
|
||||||
|
export const screenIsXlUp: Readable<boolean> = createBreakpointStore('xl');
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** These are the default Tailwind CSS breakpoints.
|
||||||
|
* We're defining them here so they can be referenced
|
||||||
|
* programmatically in other parts of the application.
|
||||||
|
*/
|
||||||
|
export const breakpoints = {
|
||||||
|
sm: '640px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '1024px',
|
||||||
|
xl: '1280px',
|
||||||
|
'2xl': '1536px',
|
||||||
|
} as const;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
import { breakpoints } from './src/theme';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
@@ -10,6 +11,7 @@ export default {
|
|||||||
'rayhunter-dark-blue': '#3f3da0',
|
'rayhunter-dark-blue': '#3f3da0',
|
||||||
'rayhunter-green': '#94ea18',
|
'rayhunter-green': '#94ea18',
|
||||||
},
|
},
|
||||||
|
screens: breakpoints,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
[Introduction](./introduction.md)
|
[Introduction](./introduction.md)
|
||||||
- [Installation](./installation.md)
|
- [Installation](./installation.md)
|
||||||
- [Installing from the latest release](./installing-from-release.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)
|
- [Installing from source](./installing-from-source.md)
|
||||||
- [Updating Rayhunter](./updating-rayhunter.md)
|
- [Updating Rayhunter](./updating-rayhunter.md)
|
||||||
- [Configuration](./configuration.md)
|
- [Configuration](./configuration.md)
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ Rayhunter can be configured through web user interface or by editing `/data/rayh
|
|||||||
Through web UI you can set:
|
Through web UI you can set:
|
||||||
- **Device UI Level**, which defines what Rayhunter shows on device's built-in screen. *Device UI Level* could be:
|
- **Device UI Level**, which defines what Rayhunter shows on device's built-in screen. *Device UI Level* could be:
|
||||||
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
|
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
|
||||||
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording
|
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording.
|
||||||
- *Demo mode (orca gif)*, which shows image of orca fish *and* colored line
|
- *Demo mode (orca gif)*, which shows image of orcas *and* colored line.
|
||||||
- *EFF logo*, which shows EFF logo and *and* colored line.
|
- *EFF logo*, which shows EFF logo and *and* colored line.
|
||||||
- **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be:
|
- **Device Input Mode**, which defines behavior of built-in power button of the device. *Device Input Mode* could be:
|
||||||
- *Disable button control*: built-in power button of the device is not used by Rayhunter;
|
- *Disable button control*: built-in power button of the device is not used by Rayhunter.
|
||||||
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristichs is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
||||||
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
|
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
|
||||||
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
|
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
|
||||||
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
|
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
|
||||||
- *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
|
- *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
|
||||||
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
|
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
|
||||||
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behaviour in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so new release may reduce false positives in existing heuristics as well.
|
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behavior in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so a new release may reduce false positives in existing heuristics as well.
|
||||||
|
|
||||||
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
|
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
|
||||||
|
|
||||||
### How can I test that my device is working?
|
### How can I test that my device is working?
|
||||||
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisey once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
|
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisy once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
|
||||||
|
|
||||||
<a name="red"></a>
|
<a name="red"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Rayhunter includes several analyzers to detect potential IMSI catcher activity.
|
|||||||
|
|
||||||
### IMSI Requested (v3)
|
### IMSI Requested (v3)
|
||||||
|
|
||||||
This analyser tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicious .
|
This analyzer tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicious .
|
||||||
|
|
||||||
Mobile networks primarily request IMSI or IMEI from a mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology).
|
Mobile networks primarily request IMSI or IMEI from a mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology).
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ What we consider suspicious is the following chain of events:
|
|||||||
* Authentication does *NOT* happen.
|
* Authentication does *NOT* happen.
|
||||||
* Tower requests phone to disconnect.
|
* Tower requests phone to disconnect.
|
||||||
|
|
||||||
Looking for this chain of events is much less prone to false positives than naively looking for any time the IMSI/IMEI is sent. We do still sometimes get false positives when users are in an airplane that is coming in for a landing however. This is likely do to having been disconnected for a while and then being over towers that are not able to route to your home network, but we are still researching.
|
Looking for this chain of events is much less prone to false positives than naively looking for any time the IMSI/IMEI is sent. We do still sometimes get false positives when users are in an airplane that is coming in for a landing however. This is likely due to having been disconnected for a while and then being over towers that are not able to route to your home network, but we are still researching.
|
||||||
|
|
||||||
This is the attack used by commercial IMSI catchers used by law enforcement.
|
This is the attack used by commercial IMSI catchers used by law enforcement.
|
||||||
|
|
||||||
@@ -36,43 +36,43 @@ This heuristic will also issue a notification every time your identity is sent t
|
|||||||
|
|
||||||
### Connection Release/Redirected Carrier 2G Downgrade
|
### Connection Release/Redirected Carrier 2G Downgrade
|
||||||
|
|
||||||
This analyser tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
This analyzer tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||||
|
|
||||||
|
|
||||||
### LTE SIB6/7 Downgrade
|
### LTE SIB6/7 Downgrade
|
||||||
|
|
||||||
This analyser tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
This analyzer tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
||||||
|
|
||||||
SIB (*System Information Block*) Type 6 and 7 are specific types of broadcast messages sent by the base station (eNodeB in 4G networks) to mobile devices. They contain essential radio-related configuration parameters to help mobile device perform cell reselection.
|
SIB (*System Information Block*) Type 6 and 7 are specific types of broadcast messages sent by the base station (eNodeB in 4G networks) to mobile devices. They contain essential radio-related configuration parameters to help mobile device perform cell reselection.
|
||||||
|
|
||||||
This attack exploits the fact that SIB broadcast messages are not encrypted or authenticated. This allows them to pretend to be a legitimate cell by broadcasting fake system information in order to force mobile devices to downgrade from more secure 4G (LTE) to less secure 2G (GSM) network and then steal IMSI and/or perform man-in-the-middle attack. That is why this is also called a downgrade attack.
|
This attack exploits the fact that SIB broadcast messages are not encrypted or authenticated. This allows them to pretend to be a legitimate cell by broadcasting fake system information in order to force mobile devices to downgrade from more secure 4G (LTE) to less secure 2G (GSM) network and then steal IMSI and/or perform man-in-the-middle attack. That is why this is also called a downgrade attack.
|
||||||
|
|
||||||
SIB6 is used for cell reselecion to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
SIB6 is used for cell reselection to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
||||||
|
|
||||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristics may trigger false positives. In that case you should consider disabling it. However this heuristics has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristic may trigger false positives. In that case you should consider disabling it. However this heuristic has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||||
|
|
||||||
### Null Cipher
|
### Null Cipher
|
||||||
|
|
||||||
This analyser tests whether the cell suggests using a null cipher (EEA0) in the RRC layer. That means that encryption between your mobile device and base station is turned off.
|
This analyzer tests whether the cell suggests using a null cipher (EEA0) in the RRC layer. That means that encryption between your mobile device and base station is turned off.
|
||||||
|
|
||||||
Normally this should never happen, because null cipher is used almost exclusively for testing and debugging in labs or in controlled environments. Sometimes null cipher is used if encryption negotiation fails or isn’t supported (however in most networks this should not be the case). Also, some regulations allow unencrypted communications in **specific** emergency cases.
|
Normally this should never happen, because null cipher is used almost exclusively for testing and debugging in labs or in controlled environments. Sometimes null cipher is used if encryption negotiation fails or isn’t supported (however in most networks this should not be the case). Also, some regulations allow unencrypted communications in **specific** emergency cases.
|
||||||
|
|
||||||
The general rule is, that null cipher should never be used in commercial deployments, except in very controlled conditions (e.g., test labs) or in a very specific regulatory-approved use cases.
|
The general rule is that null cipher should never be used in commercial deployments, except in very controlled conditions (e.g., test labs) or in a very specific regulatory-approved use cases.
|
||||||
|
|
||||||
On the other hand, IMSI catchers often use null cipher to avoid setting up secure contexts (because they lack valid keys) and/or to trick mobile device into using unencrypted links (which makes eavesdropping easier).
|
On the other hand, IMSI catchers often use null cipher to avoid setting up secure contexts (because they lack valid keys) and/or to trick mobile device into using unencrypted links (which makes eavesdropping easier).
|
||||||
|
|
||||||
### NAS Null Cipher
|
### NAS Null Cipher
|
||||||
|
|
||||||
This analyser tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a mobile device has successfully authenticated with the MME (*Mobility Management Entity* - core network component that handles signaling and control) but still it shouldn't happen at all. This could be indicative of an attack though using SS7 (*Signaling System 7* - a set of telecommunication protocols used to set up and manage calls and other services) to get key material from the HLR (*Home Location Register* - a database in mobile telecommunications networks that stores subscriber information) of the mobile phone for a successful authentication.
|
This analyzer tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a mobile device has successfully authenticated with the MME (*Mobility Management Entity* - core network component that handles signaling and control) but still it shouldn't happen at all. This could be indicative of an attack though using SS7 (*Signaling System 7* - a set of telecommunication protocols used to set up and manage calls and other services) to get key material from the HLR (*Home Location Register* - a database in mobile telecommunications networks that stores subscriber information) of the mobile phone for a successful authentication.
|
||||||
|
|
||||||
It could also indicate an IMSI catcher which is connected to the mobile network MME and HLR through cooperation between government and telecom provider. Or it could be a false positive if the telecom provider is intending to use null ciphers (if encryption is illegal in some country, or they have some misconfiguration of the network), however this should be very rare case.
|
It could also indicate an IMSI catcher which is connected to the mobile network MME and HLR through cooperation between government and telecom provider. Or it could be a false positive if the telecom provider is intending to use null ciphers (if encryption is illegal in some country, or they have some misconfiguration of the network), however this should be very rare case.
|
||||||
|
|
||||||
### Incomplete SIB
|
### Incomplete SIB
|
||||||
|
|
||||||
This analyser tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.). A legitimate SIB1 message should contain timing information for at least 2 additional SIBs (SIB3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2 (i. e. some IMSI catchers send just SIB1 and *one additional* SIB).
|
This analyzer tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.). A legitimate SIB1 message should contain timing information for at least 2 additional SIBs (SIB3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2 (i. e. some IMSI catchers send just SIB1 and *one additional* SIB).
|
||||||
|
|
||||||
On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **IMSI Requested** detection it should be considered as a strong indicator of malicious activity.
|
On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **IMSI Requested** detection it should be considered as a strong indicator of malicious activity.
|
||||||
|
|
||||||
### Test Analyzer
|
### Test Analyzer
|
||||||
|
|
||||||
This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisey so we do not recommend leaving it on but if this alerts it means your Rayhunter device is working!
|
This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisy so we do not recommend leaving it on but if this alerts it means your Rayhunter device is working!
|
||||||
|
|||||||
@@ -3,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:
|
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 (recommended)](./installing-from-release.md)
|
||||||
* [installing from a release on Windows](./installing-from-release-windows.md)
|
|
||||||
* [installing from source](./installing-from-source.md)
|
* [installing from source](./installing-from-source.md)
|
||||||
|
|
||||||
|
Already have Rayhunter installed but looking to update?
|
||||||
|
|
||||||
|
* [Updating Rayhunter](./updating-rayhunter.md)
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Installing from the latest release (Windows)
|
|
||||||
|
|
||||||
Windows support in Rayhunter's installer is a work-in-progress. Depending on the device, the installation instructions differ.
|
|
||||||
|
|
||||||
## TP-Link
|
|
||||||
|
|
||||||
1. Insert a FAT-formatted SD card. This will be used to store all recordings.
|
|
||||||
2. Connect the device via WiFi or USB Tethering -- you should be able to view the TP-Link admin page on <http://192.168.0.1>.
|
|
||||||
3. Download the latest release (must be at least 0.3.0) for windows-x86_64, and unpack the zipfile.
|
|
||||||
4. Open PowerShell or CMD in that extracted folder, the installer: `./installer tplink`
|
|
||||||
5. Follow the instructions on the screen, if there are any.
|
|
||||||
|
|
||||||
## Orbic
|
|
||||||
|
|
||||||
<div class=warning><strong>
|
|
||||||
|
|
||||||
[The Windows USB installer is known to be buggy](https://github.com/EFForg/rayhunter/issues/366). We strongly recommend using the [Network-based installer](./orbic.md#the-network-installer).
|
|
||||||
|
|
||||||
</strong></div>
|
|
||||||
|
|
||||||
1. Connect the device to your computer using the provided USB cable.
|
|
||||||
1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
|
|
||||||
1. Open Zadig, click options->show all devices
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. Select 'RNDIS (Interface 0)'
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. Click 'install driver' and wait for it to finish.
|
|
||||||
2. Download the latest `rayhunter-vX.X.X-windows-x86_64.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases). The version you download will have numbers instead of X
|
|
||||||
3. Unzip `rayhunter-vX.X.X-windows-x86_64` .
|
|
||||||
1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
|
|
||||||
5. Type `cd ~\Downloads\rayhunter-v<x.x.x>-windows-x86_64` (**Replace <x.x.x> with the Rayhunter version you just unzipped**) and hit enter.
|
|
||||||
5. Run the install script: `.\installer.exe orbic` and hit enter.
|
|
||||||
- The device will restart multiple times over the next few minutes.
|
|
||||||
- You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
|
|
||||||
6. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](./using-rayhunter.md#the-web-ui). You should also see a green line flash along the top of top the display on the device.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Make sure you've got one of Rayhunter's [supported devices](./supported-devices.md). These instructions have only been tested on macOS and Ubuntu 24.04. If they fail, you will need to [install Rayhunter from source](./installing-from-source.md).
|
Make sure you've got one of Rayhunter's [supported devices](./supported-devices.md). These instructions have only been tested on macOS and Ubuntu 24.04. If they fail, you will need to [install Rayhunter from source](./installing-from-source.md).
|
||||||
|
|
||||||
1. For the TP-Link only, insert a FAT-formatted SD card. This will be used to store all recordings.
|
1. **For the TP-Link only,** insert a FAT-formatted SD card. This will be used to store all recordings.
|
||||||
2. Download the latest `rayhunter-vX.X.X-PLATFORM.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases) for your platform:
|
2. Download the latest `rayhunter-vX.X.X-PLATFORM.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases) for your platform:
|
||||||
- for Linux on x64 architecture: `linux-x64`
|
- for Linux on x64 architecture: `linux-x64`
|
||||||
- for Linux on ARM64 architecture: `linux-aarch64`
|
- for Linux on ARM64 architecture: `linux-aarch64`
|
||||||
@@ -18,29 +18,50 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
|||||||
cd ~/Downloads/rayhunter-vX.X.X-PLATFORM
|
cd ~/Downloads/rayhunter-vX.X.X-PLATFORM
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Turn on your device by holding the power button on the front.
|
On Windows you can decompress using the file browser, then navigate to the
|
||||||
|
folder that contains `installer.exe`, **hold Shift**, Right-Click inside the
|
||||||
|
folder, then click "Open in PowerShell".
|
||||||
|
|
||||||
* For the Orbic, connect the device using a USB-C cable.
|
4. **Connect to your device.**
|
||||||
* Or connect to the network if using the network based installer, this is especially recommended on Windows.
|
|
||||||
* For TP-Link, connect to its network using either WiFi or USB Tethering.
|
|
||||||
|
|
||||||
5. Run the installer:
|
First turn on your device by holding the power button on the front.
|
||||||
|
|
||||||
```bash
|
Then connect to the device using either WiFi or USB tethering.
|
||||||
# On MacOS, you must first remove the quarantine bit
|
|
||||||
xattr -d com.apple.quarantine installer
|
|
||||||
```
|
|
||||||
Then run the installer:
|
|
||||||
```bash
|
|
||||||
./installer orbic
|
|
||||||
# or: ./installer [orbic-network|tplink|tmobile|uz801|pinephone|wingtech]
|
|
||||||
```
|
|
||||||
|
|
||||||
The device will restart multiple times over the next few minutes.
|
You know you are in the right network when you can access
|
||||||
|
<http://192.168.1.1> (Orbic) or <http://192.168.0.1> (TP-Link) and see the
|
||||||
|
hardware's own admin menu.
|
||||||
|
|
||||||
You will know it is done when you see terminal output that says `Testing Rayhunter... done`
|
5. **On MacOS only**, you have to run `xattr -d
|
||||||
|
com.apple.quarantine installer` to allow execution of
|
||||||
|
the binary.
|
||||||
|
|
||||||
6. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). You should also see a green line flash along the top of top the display on the device.
|
6. **Run the installer.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Orbic:
|
||||||
|
./installer orbic --admin-password 'mypassword'
|
||||||
|
# Or install over USB if you want ADB and a root shell (not recommended for most users)
|
||||||
|
./installer orbic-usb
|
||||||
|
|
||||||
|
# For TP-Link:
|
||||||
|
./installer tplink
|
||||||
|
```
|
||||||
|
|
||||||
|
* On Verizon Orbic, the password is the WiFi password.
|
||||||
|
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
|
||||||
|
* On Moxee-brand devices, check under the battery for the password.
|
||||||
|
* You can reset the password by pressing the button under the back case until the unit restarts.
|
||||||
|
|
||||||
|
TP-Link does not require an `--admin-password` parameter.
|
||||||
|
|
||||||
|
For other devices, check `./installer --help` or the
|
||||||
|
respective page in the sidebar under "Supported
|
||||||
|
Devices."
|
||||||
|
|
||||||
|
7. The installer will eventually tell you it's done, and the device will reboot.
|
||||||
|
|
||||||
|
8. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). You should also see a green line flash along the top of top the display on the device.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
Connect to the hotspot's network using WiFi or USB tethering and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer orbic-network --admin-password 'mypassword'
|
./installer orbic --admin-password 'mypassword'
|
||||||
```
|
```
|
||||||
|
|
||||||
The password (in place of `mypassword`) is under the battery.
|
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
|
## Obtaining a shell
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer util orbic-start-telnet
|
./installer util orbic-shell
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,30 +21,27 @@ or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
|||||||
| Wifi 5Ghz | a/ac/ax |
|
| Wifi 5Ghz | a/ac/ax |
|
||||||
| Wifi 6 | 🮱 |
|
| Wifi 6 | 🮱 |
|
||||||
|
|
||||||
## The Network Installer
|
## Two kinds of installers
|
||||||
|
|
||||||
Since Rayhunter 0.6.0 there is an alternative, experimental installation
|
The orbic's installation routine underwent many different changes:
|
||||||
procedure at `./installer orbic-network` that is supposed to eventually replace
|
|
||||||
`./installer orbic`. It does not require any USB driver installation and works
|
|
||||||
identically on Windows, Mac and Linux. From our testing it works much more
|
|
||||||
reliably on Windows than `./installer orbic` does.
|
|
||||||
|
|
||||||
The drawback is that the device's admin password is required.
|
1. The ADB-based shellscript prior to version 0.3.0
|
||||||
|
2. The Rust-based, ADB-based installer since version 0.3.0
|
||||||
|
3. Then, starting with 0.6.0, an alternative installer `./installer
|
||||||
|
orbic-network` that is supposed to work more reliably, can run over the
|
||||||
|
Orbic's WiFi connection and without the need to manually install USB drivers
|
||||||
|
on Windows.
|
||||||
|
4. Starting with 0.8.0, `orbic-network` has been renamed to `orbic`, and the
|
||||||
|
old `./installer orbic` is now called `./installer orbic-usb`.
|
||||||
|
|
||||||
1. Connect to the Orbic's network via WiFi or USB tethering
|
It's possible that many tutorials out there still refer to some of the old
|
||||||
2. Run `./installer orbic-network --admin-password 'mypassword'`
|
installation routines.
|
||||||
|
|
||||||
* On Verizon Orbic, the password is the WiFi password.
|
|
||||||
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
|
|
||||||
* On Moxee-brand devices, check under the battery for the password.
|
|
||||||
* You can reset the password by pressing the button under the back case until the unit restarts.
|
|
||||||
|
|
||||||
3. The installer will eventually reboot the device, at which point the device is up and running.
|
|
||||||
|
|
||||||
## Obtaining a shell
|
## Obtaining a shell
|
||||||
|
|
||||||
After running through the installation procedure, you can obtain a root shell
|
After running the installer, there will not be a rootshell and ADB will not be
|
||||||
by running `adb shell` or `./installer util shell`. Then, inside of that shell
|
enabled. Instead you can use `./installer util orbic-shell`.
|
||||||
you can run `/bin/rootshell` to obtain "fakeroot."
|
|
||||||
|
|
||||||
If you are using the network installer, there will not be a rootshell and ADB will not be enabled by the installer. Instead you can use `./installer util orbic-start-telnet` and connect to the hotspot using `nc 192.168.1.1 23`. On Windows you might not have `nc` and will have to use WSL for that.
|
If you are using an installer prior to 0.7.0 or `orbic-usb` explicitly, you can
|
||||||
|
obtain a root shell by running `adb shell` or `./installer util shell`. Then,
|
||||||
|
inside of that shell you can run `/bin/rootshell` to obtain "fakeroot."
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ using the `rayhunter-check` CLI tool. That tool contains the same heuristics as
|
|||||||
Rayhunter and will also work on traffic data captured with other tools, such as
|
Rayhunter and will also work on traffic data captured with other tools, such as
|
||||||
QCSuper.
|
QCSuper.
|
||||||
|
|
||||||
Since, 0.6.1, `rayhunter-check` is included in the release zipfile.
|
Since 0.6.1, `rayhunter-check` is included in the release zipfile.
|
||||||
|
|
||||||
You can build `rayhunter-check` from source with the following command:
|
You can build `rayhunter-check` from source with the following command:
|
||||||
`cargo build --bin rayhunter-check`
|
`cargo build --bin rayhunter-check`
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ According to FCC ID 2APXW-TMOHS1 Test Report No. I20Z61602-WMD02 ([part 1](https
|
|||||||
| 71 | 600 MHz (USDD) |
|
| 71 | 600 MHz (USDD) |
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
Connect to the TMOHS1's network over wifi or usb tethering.
|
Connect to the TMOHS1's network over WiFi or USB tethering.
|
||||||
|
|
||||||
The device will not accept web requests until after the default password is changed.
|
The device will not accept web requests until after the default password is changed.
|
||||||
If you have not previously logged in, log in using the default password printed under the battery and change the admin password.
|
If you have not previously logged in, log in using the default password printed under the battery and change the admin password.
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ When filing bug reports, particularly with the installer, please always specify
|
|||||||
You can get your TP-Link M7350 from:
|
You can get your TP-Link M7350 from:
|
||||||
|
|
||||||
* First check for used offers on local sites, sometimes it's much cheaper there.
|
* First check for used offers on local sites, sometimes it's much cheaper there.
|
||||||
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350)
|
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350).
|
||||||
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
|
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313).
|
||||||
|
|
||||||
## Installation & Usage
|
## Installation & Usage
|
||||||
|
|
||||||
@@ -42,11 +42,10 @@ Follow the [release installation guide](./installing-from-release.md). Substitut
|
|||||||
|
|
||||||
## Obtaining a shell
|
## 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
|
```sh
|
||||||
./installer util tplink-start-telnet
|
./installer util tplink-shell
|
||||||
telnet 192.168.0.1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Display states
|
## 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
|
add two port triggers. You can look at `Settings > NAT Settings > Port
|
||||||
Triggers` in TP-Link's admin UI to see them.
|
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.
|
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
|
## Other links
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
# Uninstalling
|
# Uninstalling
|
||||||
|
|
||||||
|
There is no automated uninstallation routine, so this page documents the routine for some devices.
|
||||||
|
|
||||||
## Orbic
|
## 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
|
```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.sh
|
rm -rf /data/rayhunter /etc/init.d/rayhunter_daemon /bin/rootshell
|
||||||
reboot
|
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
|
## TPLink
|
||||||
|
|
||||||
1. Run `./installer util tplink-start-telnet`
|
1. Run `./installer util tplink-shell` to obtain rootshell on the device.
|
||||||
2. Telnet into the device `telnet 192.168.0.1`
|
|
||||||
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
|
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
|
||||||
4. `update-rc.d rayhunter_daemon remove`
|
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.
|
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ You can access this UI in one of two ways:
|
|||||||
network and visit <http://192.168.1.1:8080> (orbic)
|
network and visit <http://192.168.1.1:8080> (orbic)
|
||||||
or <http://192.168.0.1:8080> (tplink).
|
or <http://192.168.0.1:8080> (tplink).
|
||||||
|
|
||||||
Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet.
|
Click past your browser warning you about the connection not being secure; Rayhunter doesn't have HTTPS yet.
|
||||||
|
|
||||||
On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
||||||
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ The most frequent bands found on these devices are LTE bands 1/3/5/8/20. In the
|
|||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
With the device fully booted (i.e. beaming a wifi network, blue LED, etc.) and plugged into the computer that is performing the installation, run:
|
With the device fully booted (i.e. beaming a WiFi network, blue LED, etc.) and plugged into the computer that is performing the installation, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer uz801
|
./installer uz801
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Wingtechs are abundant on ebay and can also be found on Amazon:
|
|||||||
- <https://www.amazon.com/AT-Turbo-Hotspot-256-Black/dp/B09YWLXVWT>
|
- <https://www.amazon.com/AT-Turbo-Hotspot-256-Black/dp/B09YWLXVWT>
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
Connect to the Wingtech's network over wifi or usb tethering, then run the installer:
|
Connect to the Wingtech's network over WiFi or USB tethering, then run the installer:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer wingtech --admin-password 12345678 # replace with your own password
|
./installer wingtech --admin-password 12345678 # replace with your own password
|
||||||
@@ -101,4 +101,4 @@ Caused by:
|
|||||||
3: Network is unreachable (os error 101)
|
3: Network is unreachable (os error 101)
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure your computer is connected to the hotspot's wifi network.
|
Make sure your computer is connected to the hotspot's WiFi network.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 62 KiB |
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
@@ -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" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 71 KiB |
@@ -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");
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-rayhunter-blue: #4e4eb1;
|
||||||
|
--color-rayhunter-dark-blue: #3f3da0;
|
||||||
|
--color-rayhunter-green: #94ea18;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -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;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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]
|
[package]
|
||||||
name = "installer"
|
name = "installer"
|
||||||
version = "0.7.1"
|
version = "0.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "installer"
|
||||||
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "installer"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "0.8.4"
|
aes = "0.8.4"
|
||||||
anyhow = "1.0.98"
|
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 = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false }
|
||||||
tokio-retry2 = "0.5.7"
|
tokio-retry2 = "0.5.7"
|
||||||
tokio-stream = "0.1.17"
|
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"
|
git = "https://github.com/EFForg/adb_client.git"
|
||||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||||
default-features = false
|
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
|
|
||||||
@@ -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,261 +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 Orbic RC400L.
|
|
||||||
Orbic(InstallOrbic),
|
|
||||||
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
|
|
||||||
///
|
|
||||||
/// This is an experimental installer for Orbic that does not require USB drivers on Windows.
|
|
||||||
OrbicNetwork(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: 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::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
|
||||||
Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?,
|
|
||||||
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).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
if let Err(e) = run().await {
|
if let Err(e) = installer::main_cli().await {
|
||||||
eprintln!("{e:?}");
|
eprintln!("{e:?}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::io::stdin;
|
use std::io::stdin;
|
||||||
|
|
||||||
use std::io::{ErrorKind, Write};
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::time::sleep;
|
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};
|
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||||
|
|
||||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||||
@@ -54,13 +55,17 @@ const RNDIS_INTERFACE: u8 = 1;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn confirm() -> Result<bool> {
|
async fn confirm() -> Result<bool> {
|
||||||
println!("{}", WINDOWS_WARNING);
|
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();
|
let mut input = String::new();
|
||||||
stdin().read_line(&mut input)?;
|
stdin().read_line(&mut input)?;
|
||||||
Ok(input.trim() == "yes")
|
Ok(input.trim() == "yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn install() -> Result<()> {
|
pub async fn install() -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes."
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let confirmation = confirm().await?;
|
let confirmation = confirm().await?;
|
||||||
@@ -71,19 +76,23 @@ pub async fn install() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut adb_device = force_debug_mode().await?;
|
let mut adb_device = force_debug_mode().await?;
|
||||||
echo!("Installing rootshell... ");
|
print!("Installing rootshell... ");
|
||||||
setup_rootshell(&mut adb_device).await?;
|
setup_rootshell(&mut adb_device).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
echo!("Installing rayhunter... ");
|
print!("Installing rayhunter... ");
|
||||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
echo!("Testing rayhunter... ");
|
print!("Testing rayhunter... ");
|
||||||
test_rayhunter(&mut adb_device).await?;
|
test_rayhunter(&mut adb_device).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn shell() -> Result<()> {
|
pub async fn shell() -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer util orbic-shell instead, unless you want ADB access for other purposes."
|
||||||
|
);
|
||||||
|
|
||||||
println!("opening shell");
|
println!("opening shell");
|
||||||
let mut adb_device = get_adb().await?;
|
let mut adb_device = get_adb().await?;
|
||||||
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
|
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
|
||||||
@@ -93,11 +102,11 @@ pub async fn shell() -> Result<()> {
|
|||||||
async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||||
println!("Forcing a switch into the debug mode to enable ADB");
|
println!("Forcing a switch into the debug mode to enable ADB");
|
||||||
enable_command_mode()?;
|
enable_command_mode()?;
|
||||||
echo!("ADB enabled, waiting for reboot... ");
|
print!("ADB enabled, waiting for reboot... ");
|
||||||
let mut adb_device = get_adb().await?;
|
let mut adb_device = get_adb().await?;
|
||||||
adb_setup_serial(&mut adb_device).await?;
|
adb_setup_serial(&mut adb_device).await?;
|
||||||
println!("it's alive!");
|
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"])?;
|
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
|
||||||
println!("done");
|
println!("done");
|
||||||
Ok(adb_device)
|
Ok(adb_device)
|
||||||
@@ -151,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/rayhunter_daemon").await?;
|
||||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
echo!("Waiting for reboot... ");
|
print!("Waiting for reboot... ");
|
||||||
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
|
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
|
||||||
// first wait for shutdown (it can take ~10s)
|
// first wait for shutdown (it can take ~10s)
|
||||||
tokio::time::timeout(Duration::from_secs(30), async {
|
tokio::time::timeout(Duration::from_secs(30), async {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::io::Write;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -9,9 +8,13 @@ use serde::Deserialize;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
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};
|
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)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ExploitResponse {
|
struct ExploitResponse {
|
||||||
retcode: u32,
|
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))
|
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Cookie", authenticated_cookie)
|
.header("Cookie", authenticated_cookie)
|
||||||
// Original Orbic lacks telnetd (unlike other devices)
|
// Original Orbic lacks telnetd (kajeet has it) so we need to use netcat
|
||||||
// When doing this, one needs to set prompt=None in the telnet utility functions
|
.body(format!(
|
||||||
// But some kajeet devices have password protected telnetd so we use port 24 just in case
|
r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
|
||||||
.body(r#"{"password": "\"; busybox nc -ll -p 24 -e /bin/sh & #"}"#)
|
))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("failed to start telnet")?
|
.context("failed to start telnet")?
|
||||||
@@ -122,9 +125,13 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
|
|||||||
pub async fn start_telnet(
|
pub async fn start_telnet(
|
||||||
admin_ip: &str,
|
admin_ip: &str,
|
||||||
admin_username: &str,
|
admin_username: &str,
|
||||||
admin_password: &str,
|
admin_password: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
echo!("Logging in and starting telnet... ");
|
let Some(admin_password) = admin_password else {
|
||||||
|
anyhow::bail!("--admin-password is required");
|
||||||
|
};
|
||||||
|
|
||||||
|
print!("Logging in and starting telnet... ");
|
||||||
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
|
|
||||||
@@ -134,13 +141,27 @@ pub async fn start_telnet(
|
|||||||
pub async fn install(
|
pub async fn install(
|
||||||
admin_ip: String,
|
admin_ip: String,
|
||||||
admin_username: String,
|
admin_username: String,
|
||||||
admin_password: String,
|
admin_password: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
echo!("Logging in and starting telnet... ");
|
let Some(admin_password) = admin_password else {
|
||||||
|
eprintln!(
|
||||||
|
"As of version 0.8.0, the orbic installer has been rewritten and now requires an --admin-password parameter."
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"Refer to the official documentation at https://efforg.github.io/rayhunter/ for how to find the right value."
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"If you are following a tutorial that does not include this parameter, the tutorial is likely outdated. You can run ./installer orbic-usb to access the old installer, however we recommend against it."
|
||||||
|
);
|
||||||
|
anyhow::bail!("exiting");
|
||||||
|
};
|
||||||
|
|
||||||
|
print!("Logging in and starting telnet... ");
|
||||||
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
|
|
||||||
echo!("Waiting for telnet to become available... ");
|
print!("Waiting for telnet to become available... ");
|
||||||
wait_for_telnet(&admin_ip).await?;
|
wait_for_telnet(&admin_ip).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
|
|
||||||
@@ -148,7 +169,7 @@ pub async fn install(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
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 timeout = Duration::from_secs(60);
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
@@ -169,7 +190,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_rayhunter(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"));
|
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||||
|
|
||||||
// Remount filesystem as read-write to allow modifications
|
// Remount filesystem as read-write to allow modifications
|
||||||
@@ -252,3 +273,16 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -11,7 +10,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::orbic::test_rayhunter;
|
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};
|
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||||
|
|
||||||
const USB_VENDOR_ID: u16 = 0x2C7C;
|
const USB_VENDOR_ID: u16 = 0x2C7C;
|
||||||
@@ -19,7 +19,7 @@ const USB_PRODUCT_ID: u16 = 0x125;
|
|||||||
const USB_INTERFACE_NUMBER: u8 = 2;
|
const USB_INTERFACE_NUMBER: u8 = 2;
|
||||||
|
|
||||||
pub async fn install() -> Result<()> {
|
pub async fn install() -> Result<()> {
|
||||||
echo!("Unlocking modem ... ");
|
print!("Unlocking modem ... ");
|
||||||
start_adb().await?;
|
start_adb().await?;
|
||||||
sleep(Duration::from_secs(3)).await;
|
sleep(Duration::from_secs(3)).await;
|
||||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
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")?;
|
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
|
||||||
sleep(Duration::from_secs(30)).await;
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
|
||||||
echo!("Unlocking modem ... ");
|
print!("Unlocking modem ... ");
|
||||||
start_adb().await?;
|
start_adb().await?;
|
||||||
sleep(Duration::from_secs(3)).await;
|
sleep(Duration::from_secs(3)).await;
|
||||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||||
println!("ok");
|
println!("ok");
|
||||||
|
|
||||||
echo!("Testing rayhunter ... ");
|
print!("Testing rayhunter ... ");
|
||||||
test_rayhunter(&mut adb).await?;
|
test_rayhunter(&mut adb).await?;
|
||||||
println!("ok");
|
println!("ok");
|
||||||
println!("rayhunter is running on the modem. Use adb to access the web interface.");
|
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.
|
/// Transfer a file to the modem's filesystem with adb push.
|
||||||
/// Validates the file sends successfully to /tmp before overwriting the destination.
|
/// Validates the file sends successfully to /tmp before overwriting the destination.
|
||||||
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
|
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)
|
let file_name = Path::new(dest)
|
||||||
.file_name()
|
.file_name()
|
||||||
.ok_or_else(|| anyhow!("{dest} does not have a 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_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP
|
||||||
/// WT_PRODUCTION_VERSION=TMOHS1_00.05.20
|
/// WT_PRODUCTION_VERSION=TMOHS1_00.05.20
|
||||||
/// WT_HARDWARE_VERSION=89527_1_11
|
/// WT_HARDWARE_VERSION=89527_1_11
|
||||||
use std::io::Write;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -13,7 +12,8 @@ use anyhow::Result;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::TmobileArgs as Args;
|
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;
|
use crate::wingtech::start_telnet;
|
||||||
|
|
||||||
pub async fn install(
|
pub async fn install(
|
||||||
@@ -26,12 +26,12 @@ pub async fn install(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||||
echo!("Starting telnet ... ");
|
print!("Starting telnet ... ");
|
||||||
start_telnet(&admin_ip, &admin_password).await?;
|
start_telnet(&admin_ip, &admin_password).await?;
|
||||||
sleep(Duration::from_millis(200)).await;
|
sleep(Duration::from_millis(200)).await;
|
||||||
println!("ok");
|
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();
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||||
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
|
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
|
||||||
println!("ok");
|
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?;
|
telnet_send_command(addr, "reboot", "exit code 0", true).await?;
|
||||||
sleep(Duration::from_secs(30)).await;
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
|
||||||
echo!("Testing rayhunter ... ");
|
print!("Testing rayhunter ... ");
|
||||||
let max_failures = 10;
|
let max_failures = 10;
|
||||||
http_ok_every(
|
http_ok_every(
|
||||||
format!("http://{admin_ip}:8080/index.html"),
|
format!("http://{admin_ip}:8080/index.html"),
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ use serde::Deserialize;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::InstallTpLink;
|
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>;
|
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
||||||
|
|
||||||
@@ -265,6 +266,14 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
|||||||
// on other versions, this path is /js/settings.min.js
|
// on other versions, this path is /js/settings.min.js
|
||||||
let is_settings_js = path.ends_with("/settings.min.js");
|
let is_settings_js = path.ends_with("/settings.min.js");
|
||||||
|
|
||||||
|
if is_settings_js {
|
||||||
|
// It can happen that new versions of the admin JS do not take effect because of caching
|
||||||
|
// headers. This is a problem when trying multiple versions of the installer. Delete all
|
||||||
|
// caching headers and hope the server never erroneously returns a 304 that way.
|
||||||
|
req.headers_mut().remove("If-Modified-Since");
|
||||||
|
req.headers_mut().remove("If-None-Match");
|
||||||
|
}
|
||||||
|
|
||||||
*req.uri_mut() = Uri::try_from(uri).unwrap();
|
*req.uri_mut() = Uri::try_from(uri).unwrap();
|
||||||
|
|
||||||
let mut response = state
|
let mut response = state
|
||||||
@@ -281,22 +290,33 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
|||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
let mut data = BytesMut::from(data);
|
let mut data = BytesMut::from(data);
|
||||||
// inject some javascript into the admin UI to get us a telnet shell.
|
// inject some javascript into the admin UI to get us a telnet shell.
|
||||||
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
|
data.extend(br#";document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Intentionally register rayhunter-daemon before rayhunter-root so that we are less
|
console.log("rayhunter: start polling");
|
||||||
// likely to run into race conditions where rayhunter-root is launched, and the
|
|
||||||
// installer kills the server. In practice both HTTP requests may execute concurrently
|
var rayhunterSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
// anyway.
|
|
||||||
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
|
var rayhunterPoll = window.setInterval(async () => {
|
||||||
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
|
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 1, openPort: "2401", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start &)", triggerProtocol: "TCP"});
|
||||||
|
console.log("rayhunter: first request succeeded, stopping rayhunter poll loop");
|
||||||
|
window.clearInterval(rayhunterPoll);
|
||||||
|
|
||||||
|
// PTModel.add actually does not wait for the request to finsh.
|
||||||
|
// Wait 1 second for the request to finish.
|
||||||
|
// Running both requests concurrently can get one of the two requests rejected, as
|
||||||
|
// sending a request with entryId: 2 is invalid if entryId 1 does not exist (yet)
|
||||||
|
// This only happens starting with firmware M7350(EU)_V9_9.0.2 Build 241021, earlier
|
||||||
|
// versions are not affected.
|
||||||
|
await rayhunterSleep(1000);
|
||||||
|
|
||||||
|
console.log("rayhunter: running second request");
|
||||||
|
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 2, openPort: "2402", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh &)", triggerProtocol: "TCP"});
|
||||||
|
|
||||||
// Do not use alert(), instead replace page with success message. Using alert() will
|
// Do not use alert(), instead replace page with success message. Using alert() will
|
||||||
// block the event loop in such a way that any background promises are blocked from
|
// block the event loop in such a way that any background promises are blocked from
|
||||||
// progress too. For example: The HTTP requests to register our port triggers!
|
// progress too. For example: The HTTP requests to register our port triggers!
|
||||||
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
|
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
|
||||||
|
}, 1000);
|
||||||
// We can stop polling now, presumably both requests are already inflight.
|
});"#);
|
||||||
window.clearInterval(window.rayhunterPoll);
|
|
||||||
}, 1000);"#);
|
|
||||||
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
|
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
|
||||||
response.headers_mut().remove("Content-Length");
|
response.headers_mut().remove("Content-Length");
|
||||||
}
|
}
|
||||||
@@ -359,10 +379,18 @@ fn get_rayhunter_daemon(sdcard_path: &str) -> String {
|
|||||||
// specific to a particular hardware revision here.
|
// specific to a particular hardware revision here.
|
||||||
crate::RAYHUNTER_DAEMON_INIT.replace(
|
crate::RAYHUNTER_DAEMON_INIT.replace(
|
||||||
"#RAYHUNTER-PRESTART",
|
"#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]
|
#[test]
|
||||||
fn test_get_rayhunter_daemon() {
|
fn test_get_rayhunter_daemon() {
|
||||||
let s = get_rayhunter_daemon("/media/card");
|
let s = get_rayhunter_daemon("/media/card");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::io::Write;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -10,13 +9,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::{sleep, timeout};
|
use tokio::time::{sleep, timeout};
|
||||||
|
|
||||||
macro_rules! echo {
|
use crate::output::{print, println};
|
||||||
($($arg:tt)*) => {
|
|
||||||
print!($($arg)*);
|
#[cfg(unix)]
|
||||||
let _ = std::io::stdout().flush();
|
use std::os::fd::AsRawFd;
|
||||||
};
|
|
||||||
}
|
|
||||||
pub(crate) use echo;
|
|
||||||
|
|
||||||
pub async fn telnet_send_command_with_output(
|
pub async fn telnet_send_command_with_output(
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -91,7 +87,7 @@ pub async fn telnet_send_file(
|
|||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
wait_for_prompt: bool,
|
wait_for_prompt: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
echo!("Sending file {filename} ... ");
|
print!("Sending file {filename} ... ");
|
||||||
let nc_output = {
|
let nc_output = {
|
||||||
let filename = filename.to_owned();
|
let filename = filename.to_owned();
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
@@ -102,14 +98,31 @@ pub async fn telnet_send_file(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
// wait for nc to become available. if the installer fails with connection refused, this
|
|
||||||
// likely is not high enough.
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
let mut addr = addr;
|
let mut addr = addr;
|
||||||
addr.set_port(8081);
|
addr.set_port(8081);
|
||||||
|
|
||||||
|
let mut stream;
|
||||||
|
let mut attempts = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// wait for nc to become available, with exponential backoff.
|
||||||
|
//
|
||||||
|
// if the installer fails with connection refused, this
|
||||||
|
// likely is not high enough.
|
||||||
|
sleep(Duration::from_millis(100 * (1 << attempts))).await;
|
||||||
|
|
||||||
|
stream = TcpStream::connect(addr).await;
|
||||||
|
attempts += 1;
|
||||||
|
if stream.is_ok() || attempts > 3 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
print!("attempt {attempts}... ");
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut stream = TcpStream::connect(addr).await?;
|
let mut stream = stream?;
|
||||||
stream.write_all(payload).await?;
|
stream.write_all(payload).await?;
|
||||||
|
|
||||||
// if the orbic is sluggish, we need for nc to write the data to disk before
|
// if the orbic is sluggish, we need for nc to write the data to disk before
|
||||||
@@ -122,6 +135,7 @@ pub async fn telnet_send_file(
|
|||||||
sleep(Duration::from_millis(1000)).await;
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
// ensure that stream is dropped before we wait for nc to terminate.
|
// ensure that stream is dropped before we wait for nc to terminate.
|
||||||
|
drop(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.await??
|
handle.await??
|
||||||
@@ -198,6 +212,7 @@ pub async fn http_ok_every(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// General function to open a USB device
|
/// General function to open a USB device
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
||||||
let devices = match nusb::list_devices() {
|
let devices = match nusb::list_devices() {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
@@ -213,3 +228,82 @@ pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
|||||||
}
|
}
|
||||||
Ok(None)
|
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;
|
use std::path::Path;
|
||||||
/// Installer for the Uz801 hotspot.
|
/// Installer for the Uz801 hotspot.
|
||||||
///
|
///
|
||||||
@@ -15,30 +14,30 @@ use md5::compute as md5_compute;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::Uz801Args as Args;
|
use crate::Uz801Args as Args;
|
||||||
use crate::util::echo;
|
use crate::output::{print, println};
|
||||||
|
|
||||||
pub async fn install(Args { admin_ip }: Args) -> Result<()> {
|
pub async fn install(Args { admin_ip }: Args) -> Result<()> {
|
||||||
run_install(admin_ip).await
|
run_install(admin_ip).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_install(admin_ip: String) -> Result<()> {
|
async fn run_install(admin_ip: String) -> Result<()> {
|
||||||
echo!("Activating USB debugging backdoor... ");
|
print!("Activating USB debugging backdoor... ");
|
||||||
activate_usb_debug(&admin_ip).await?;
|
activate_usb_debug(&admin_ip).await?;
|
||||||
println!("ok");
|
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?;
|
let mut adb_device = wait_for_adb().await?;
|
||||||
println!("ok");
|
println!("ok");
|
||||||
|
|
||||||
echo!("Installing rayhunter files... ");
|
print!("Installing rayhunter files... ");
|
||||||
install_rayhunter_files(&mut adb_device).await?;
|
install_rayhunter_files(&mut adb_device).await?;
|
||||||
println!("ok");
|
println!("ok");
|
||||||
|
|
||||||
echo!("Modifying startup script... ");
|
print!("Modifying startup script... ");
|
||||||
modify_startup_script(&mut adb_device).await?;
|
modify_startup_script(&mut adb_device).await?;
|
||||||
println!("ok");
|
println!("ok");
|
||||||
|
|
||||||
echo!("Rebooting the device... ");
|
print!("Rebooting the device... ");
|
||||||
let _ = adb_device.reboot(adb_client::RebootType::System);
|
let _ = adb_device.reboot(adb_client::RebootType::System);
|
||||||
println!("ok");
|
println!("ok");
|
||||||
|
|
||||||
@@ -54,6 +53,21 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
|
|||||||
let referer = format!("http://{admin_ip}/usbdebug.html");
|
let referer = format!("http://{admin_ip}/usbdebug.html");
|
||||||
let origin = format!("http://{admin_ip}");
|
let origin = format!("http://{admin_ip}");
|
||||||
|
|
||||||
|
// Check if device is online
|
||||||
|
print!("Checking if device is online... ");
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(5))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
match client.get(&origin).send().await {
|
||||||
|
Ok(response) if response.status().is_success() => println!("ok"),
|
||||||
|
Ok(response) => anyhow::bail!(
|
||||||
|
"Device at {admin_ip} returned error status: {}",
|
||||||
|
response.status()
|
||||||
|
),
|
||||||
|
Err(e) => anyhow::bail!("Failed to reach device at {admin_ip}: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
let _handle = tokio::spawn(async move {
|
let _handle = tokio::spawn(async move {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(5))
|
.timeout(Duration::from_secs(5))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||||
/// WT_HARDWARE_VERSION=89323_1_20
|
/// WT_HARDWARE_VERSION=89323_1_20
|
||||||
use std::io::Write;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -19,7 +18,8 @@ use serde::Deserialize;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::WingtechArgs as Args;
|
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)]
|
#[derive(Deserialize)]
|
||||||
struct LoginResponse {
|
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<()> {
|
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?;
|
start_telnet(&admin_ip, &admin_password).await?;
|
||||||
println!("ok");
|
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();
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||||
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
|
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
|
||||||
println!("ok");
|
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?;
|
telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0", true).await?;
|
||||||
sleep(Duration::from_secs(30)).await;
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
|
||||||
echo!("Testing rayhunter ... ");
|
print!("Testing rayhunter ... ");
|
||||||
let max_failures = 10;
|
let max_failures = 10;
|
||||||
http_ok_every(
|
http_ok_every(
|
||||||
format!("http://{admin_ip}:8080/index.html"),
|
format!("http://{admin_ip}:8080/index.html"),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.7.1"
|
version = "0.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ path = "src/lib.rs"
|
|||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
crc = "3.0.1"
|
crc = "3.0.1"
|
||||||
deku = { version = "0.18.0", features = ["logging"] }
|
deku = { version = "0.20.0", features = ["logging"] }
|
||||||
libc = "0.2.150"
|
libc = "0.2.150"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
nix = { version = "0.29.0", features = ["feature"] }
|
nix = { version = "0.29.0", features = ["feature"] }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crc::{Algorithm, Crc};
|
|||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
|
|
||||||
use crate::hdlc::{self, hdlc_decapsulate};
|
use crate::hdlc::{self, hdlc_decapsulate};
|
||||||
use log::{error, warn};
|
use log::warn;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||||
@@ -131,7 +131,7 @@ pub enum Message {
|
|||||||
log_type: u16,
|
log_type: u16,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(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,
|
body: LogBody,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -141,10 +141,13 @@ pub enum Message {
|
|||||||
// pass those opcodes down to their respective parsers.
|
// pass those opcodes down to their respective parsers.
|
||||||
#[deku(id_pat = "_")]
|
#[deku(id_pat = "_")]
|
||||||
Response {
|
Response {
|
||||||
opcode: u32,
|
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||||
|
opcode2: u8,
|
||||||
|
opcode3: u8,
|
||||||
|
opcode4: u8,
|
||||||
subopcode: u32,
|
subopcode: u32,
|
||||||
status: u32,
|
status: u32,
|
||||||
#[deku(ctx = "*opcode, *subopcode")]
|
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||||
payload: ResponsePayload,
|
payload: ResponsePayload,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -189,20 +192,22 @@ pub enum LogBody {
|
|||||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||||
Nas4GMessage {
|
Nas4GMessage {
|
||||||
#[deku(ctx = "log_type")]
|
#[deku(skip, default = "log_type")]
|
||||||
|
log_type: u16,
|
||||||
|
#[deku(ctx = "*log_type")]
|
||||||
direction: Nas4GMessageDirection,
|
direction: Nas4GMessageDirection,
|
||||||
ext_header_version: u8,
|
ext_header_version: u8,
|
||||||
rrc_rel: u8,
|
rrc_rel: u8,
|
||||||
rrc_version_minor: u8,
|
rrc_version_minor: u8,
|
||||||
rrc_version_major: u8,
|
rrc_version_major: u8,
|
||||||
// message length = hdr_len - (sizeof(ext_header_version) + sizeof(rrc_rel) + sizeof(rrc_version_minor) + sizeof(rrc_version_major))
|
// 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>,
|
msg: Vec<u8>,
|
||||||
},
|
},
|
||||||
#[deku(id = "0x11eb")]
|
#[deku(id = "0x11eb")]
|
||||||
IpTraffic {
|
IpTraffic {
|
||||||
// is this right?? based on https://github.com/P1sec/QCSuper/blob/81dbaeee15ec7747e899daa8e3495e27cdcc1264/src/modules/pcap_dump.py#L378
|
// 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>,
|
msg: Vec<u8>,
|
||||||
},
|
},
|
||||||
#[deku(id = "0x713a")]
|
#[deku(id = "0x713a")]
|
||||||
@@ -613,4 +618,83 @@ mod test {
|
|||||||
Err(DiagParsingError::HdlcDecapsulationError(_, _))
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |