Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe6afac817 | |||
| 8e708f145e | |||
| 03c00a1f19 | |||
| 64842c7140 | |||
| e108c21fc2 | |||
| 49a2108214 | |||
| 53a6cbe95a | |||
| 398997af67 | |||
| 6b109a9d76 | |||
| d9688b1796 | |||
| 7466c1c669 | |||
| 6a51050921 | |||
| 0935cf8239 | |||
| d25e9588e2 | |||
| a8ff95a07b | |||
| ac86277903 | |||
| 8e9abc718a | |||
| d92fb16c57 | |||
| f8824ce7e7 | |||
| 9694aa826b | |||
| b859dde0c8 | |||
| 5b6a73bc44 | |||
| 8cbdbf5ebe | |||
| ccce63e90c | |||
| 68b13ea09e | |||
| 672d825bdb | |||
| fd216ecb72 | |||
| 07d43b5924 | |||
| bd3e439a1d | |||
| 5491c3f3a0 | |||
| fa14e4ecfc | |||
| 8583064e46 | |||
| d3bd8d9dfc | |||
| b16a351727 | |||
| cd781fe8d8 | |||
| df00e00076 | |||
| 1a810cfb33 | |||
| b16b1af65e | |||
| a346449ec5 | |||
| 464740a1a7 | |||
| e07b0b05e7 | |||
| 578bc0d234 | |||
| 751d504440 | |||
| 29c944af45 | |||
| e239653a44 | |||
| 841bc7b015 | |||
| 22d927aa25 | |||
| 5b59efa4c8 | |||
| f273d28728 | |||
| f1e283b52c | |||
| 1011c4b123 | |||
| 5db24e4b21 | |||
| a72e4b2234 | |||
| ca0151f656 | |||
| 56930db130 | |||
| f018b8f662 | |||
| 7e0f12f1c5 | |||
| e32a6f5b2e | |||
| 58618f3412 | |||
| 003a8b280b | |||
| 27bf20fbf4 | |||
| b7636386fc | |||
| f23cc07652 | |||
| f9b621bde9 | |||
| a4cb9454bd | |||
| fbac464b46 | |||
| b923d9d5a6 | |||
| 790c0963cd | |||
| 32106ac0f4 | |||
| 1ce4d99c59 | |||
| b055ddc670 | |||
| 09d4328dc2 | |||
| 1a4deb7524 | |||
| 0585e0f996 | |||
| c783831e78 | |||
| 3ddbaa07ca | |||
| 83f246e9af | |||
| 0d96b4c103 | |||
| 7cd8835cab | |||
| e81df18315 | |||
| 0915103ede | |||
| da18a1f9da | |||
| 5bb3dc9db5 | |||
| c2c6004f4e | |||
| e320874854 | |||
| 300215206c | |||
| 5e328b889b | |||
| 97cbe62f42 | |||
| 27408dd64a | |||
| e5c0e13d32 | |||
| 41133ba793 | |||
| 0be2b02349 | |||
| 81eb3eac57 | |||
| 3247d35b7e | |||
| 355242fa71 | |||
| 72d6c65f29 | |||
| 5e66c26e70 | |||
| b0d8307a14 | |||
| cf0875f2e3 | |||
| 1c51e5ed6f | |||
| 3a393fc29f | |||
| b97421d220 | |||
| 1bf386d5b7 | |||
| 8de4dcfd18 | |||
| c0b1d4608a | |||
| ee8bf0107a | |||
| 664ffc8c75 | |||
| d03debe67c | |||
| 60922afc87 | |||
| 932fef32b9 | |||
| e259417f35 | |||
| 3889c89b5a | |||
| bd074066c5 | |||
| 8b44f604ea | |||
| ef7b8129ef | |||
| c3fd724ac1 | |||
| 28ead37111 | |||
| 6efe83b36d | |||
| 4d0427fe68 | |||
| 1ee35dad71 | |||
| 5d2a5a2577 | |||
| a4f4e12a57 | |||
| 55178e60fd | |||
| 5019f2a9d1 | |||
| 25978a4da4 | |||
| 4ad79707bb | |||
| 5f45ae31d8 | |||
| ed3072eb8e | |||
| 94289dcad5 | |||
| 2b86691e57 | |||
| 0a15ca1b1a | |||
| eeef42f4cb | |||
| 04cf0ab73a | |||
| 23a0f72c2f | |||
| efae6203a9 | |||
| 2e4de4a2df | |||
| aac0c34eaa |
@@ -15,6 +15,10 @@ rustflags = ["-C", "target-feature=+crt-static"]
|
|||||||
linker = "rust-lld"
|
linker = "rust-lld"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
[target.armv7-unknown-linux-musleabi]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
# Disable rust-lld for x86 macOS because the linker crashers when compiling
|
# Disable rust-lld for x86 macOS because the linker crashers when compiling
|
||||||
# the installer in release mode with debug info on.
|
# the installer in release mode with debug info on.
|
||||||
# [target.x86_64-apple-darwin]
|
# [target.x86_64-apple-darwin]
|
||||||
@@ -25,17 +29,22 @@ rustflags = ["-C", "target-feature=+crt-static"]
|
|||||||
linker = "rust-lld"
|
linker = "rust-lld"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
# keep line numbers in stack traces for non-firmware binaries
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
# keep line numbers in stack traces for non-firmware binaries
|
||||||
debug = "limited"
|
debug = "limited"
|
||||||
|
lto = "fat"
|
||||||
|
opt-level = "z"
|
||||||
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[profile.firmware-devel]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = "s"
|
||||||
|
lto = false
|
||||||
|
|
||||||
# optimizations to reduce the binary size of firmware binaries
|
# optimizations to reduce the binary size of firmware binaries
|
||||||
[profile.firmware]
|
[profile.firmware]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
strip = true
|
strip = true
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
9fe75ac961c57e508bf7488ce51d596750fa8d37
|
9fe75ac961c57e508bf7488ce51d596750fa8d37
|
||||||
|
76ffdf6bada515c9a5f63a600e6f1502288c147a
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Files that are distributed onto the Rayhunter device always have to have
|
||||||
|
# Unix-style line endings, even if the installer is built on Windows with
|
||||||
|
# autocrlf enabled.
|
||||||
|
# Using CRLF for the init scripts will make them fail to execute on TP-Link.
|
||||||
|
# See https://github.com/EFForg/rayhunter/issues/489
|
||||||
|
|
||||||
|
dist/config.toml.in eol=lf
|
||||||
|
dist/scripts/misc-daemon eol=lf
|
||||||
|
dist/scripts/rayhunter_daemon eol=lf
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Installer Issue
|
||||||
|
description: File an bug related to an installer issue.
|
||||||
|
labels: ["bug", "installer"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Rayhunter Version
|
||||||
|
placeholder: 'v0.5.0'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: |
|
||||||
|
What device are you trying to install Rayhunter on?
|
||||||
|
options:
|
||||||
|
- Orbic RC400L
|
||||||
|
- Tplink HW7350
|
||||||
|
- Tplink HW7310
|
||||||
|
- Tmobile TMOHS1
|
||||||
|
- Wingtech CT2MHS0
|
||||||
|
- Pinephone
|
||||||
|
- Other / I'm not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Installer OS
|
||||||
|
description: What operating system are running the installer from
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the Issue
|
||||||
|
description: |
|
||||||
|
Please describe the issue you're having installing Rayhunter.
|
||||||
|
Include the logs outputed by the installer program. If the installer
|
||||||
|
is crashing, please try running the installer with `RUST_BACKTRACE=1`
|
||||||
|
environment variable set so we can see exactly where the installer is
|
||||||
|
crashing.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -10,9 +10,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
FILE_ROOTSHELL: ../../rootshell/rootshell
|
FILE_ROOTSHELL: ../../rootshell/rootshell
|
||||||
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
|
FILE_RAYHUNTER_DAEMON: ../../rayhunter-daemon/rayhunter-daemon
|
||||||
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
|
RUSTFLAGS: "-Dwarnings"
|
||||||
FILE_RAYHUNTER_DAEMON_WINGTECH: ../../rayhunter-daemon-wingtech/rayhunter-daemon
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
files_changed:
|
files_changed:
|
||||||
@@ -23,6 +22,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
code_changed: ${{ steps.files_changed.outputs.code_count }}
|
code_changed: ${{ steps.files_changed.outputs.code_count }}
|
||||||
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
|
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
|
||||||
|
web_changed: ${{ steps.files_changed.outputs.web_count }}
|
||||||
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
||||||
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
|
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
|
||||||
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||||
@@ -36,17 +36,19 @@ jobs:
|
|||||||
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/
|
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"
|
||||||
echo daemon_count=forced >> "$GITHUB_OUTPUT"
|
echo daemon_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 rootshell_count=forced >> "$GITHUB_OUTPUT"
|
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^bin -e ^installer -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 ^bin -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 "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 "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"
|
||||||
@@ -97,12 +99,6 @@ jobs:
|
|||||||
check_and_test:
|
check_and_test:
|
||||||
needs: files_changed
|
needs: files_changed
|
||||||
if: needs.files_changed.outputs.code_changed != '0'
|
if: needs.files_changed.outputs.code_changed != '0'
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
device:
|
|
||||||
- name: orbic
|
|
||||||
- name: tplink
|
|
||||||
- name: wingtech
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -113,17 +109,33 @@ jobs:
|
|||||||
run: cargo fmt --all --check
|
run: cargo fmt --all --check
|
||||||
- name: Check
|
- name: Check
|
||||||
run: |
|
run: |
|
||||||
pushd bin/web
|
pushd daemon/web
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
popd
|
popd
|
||||||
NO_FIRMWARE_BIN=true cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
NO_FIRMWARE_BIN=true cargo check --verbose
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
NO_FIRMWARE_BIN=true cargo test --verbose
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
NO_FIRMWARE_BIN=true cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
|
NO_FIRMWARE_BIN=true cargo clippy --verbose
|
||||||
|
|
||||||
|
test_web_frontend:
|
||||||
|
needs: files_changed
|
||||||
|
if: needs.files_changed.outputs.web_changed != '0'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: daemon/web
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm run check
|
||||||
|
- run: npm run test
|
||||||
|
|
||||||
windows_installer_check_and_test:
|
windows_installer_check_and_test:
|
||||||
needs: files_changed
|
needs: files_changed
|
||||||
@@ -156,10 +168,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- name: ubuntu-24
|
- name: linux-x64
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-musl
|
target: x86_64-unknown-linux-musl
|
||||||
- name: ubuntu-24-aarch64
|
- name: linux-armv7
|
||||||
|
os: ubuntu-latest
|
||||||
|
target: armv7-unknown-linux-musleabi
|
||||||
|
- name: linux-aarch64
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
target: aarch64-unknown-linux-musl
|
target: aarch64-unknown-linux-musl
|
||||||
- name: macos-arm
|
- name: macos-arm
|
||||||
@@ -174,13 +189,16 @@ jobs:
|
|||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform.target }}
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Build rayhunter-check
|
- name: Build rayhunter-check
|
||||||
run: cargo build --bin rayhunter-check --release
|
run: cargo build --bin rayhunter-check --release --target ${{ matrix.platform.target }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rayhunter-check-${{ matrix.platform.name }}
|
name: rayhunter-check-${{ matrix.platform.name }}
|
||||||
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
path: target/${{ matrix.platform.target }}/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_rootshell:
|
build_rootshell:
|
||||||
@@ -197,7 +215,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-musleabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Build rootshell (arm32)
|
- name: Build rootshell (armv7)
|
||||||
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
|
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -213,12 +231,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
device:
|
|
||||||
- name: orbic
|
|
||||||
- name: tplink
|
|
||||||
- name: wingtech
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -226,9 +238,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-musleabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Build rayhunter-daemon (arm32)
|
- name: Build rayhunter-daemon (armv7)
|
||||||
run: |
|
run: |
|
||||||
pushd bin/web
|
pushd daemon/web
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
popd
|
popd
|
||||||
@@ -240,10 +252,10 @@ jobs:
|
|||||||
# what the feature selection in rayhunter-daemon is.
|
# what the feature selection in rayhunter-daemon is.
|
||||||
#
|
#
|
||||||
# https://github.com/rust-lang/cargo/issues/4463
|
# https://github.com/rust-lang/cargo/issues/4463
|
||||||
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware --no-default-features --features ${{ matrix.device.name }}
|
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rayhunter-daemon-${{ matrix.device.name }}
|
name: rayhunter-daemon
|
||||||
path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
|
path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
@@ -260,10 +272,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- name: ubuntu-24
|
- name: linux-x64
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-musl
|
target: x86_64-unknown-linux-musl
|
||||||
- name: ubuntu-24-aarch64
|
- name: linux-armv7
|
||||||
|
os: ubuntu-latest
|
||||||
|
target: armv7-unknown-linux-musleabi
|
||||||
|
- name: linux-aarch64
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
target: aarch64-unknown-linux-musl
|
target: aarch64-unknown-linux-musl
|
||||||
- name: macos-arm
|
- name: macos-arm
|
||||||
@@ -283,7 +298,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo build --bin installer --release --target ${{ matrix.platform.target }}
|
- run: cargo build --package installer --bin installer --release --target ${{ matrix.platform.target }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installer-${{ matrix.platform.name }}
|
name: installer-${{ matrix.platform.name }}
|
||||||
@@ -300,33 +315,38 @@ jobs:
|
|||||||
- build_rayhunter
|
- build_rayhunter
|
||||||
- build_rust_installer
|
- build_rust_installer
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux-x64
|
||||||
|
- linux-aarch64
|
||||||
|
- linux-armv7
|
||||||
|
- macos-intel
|
||||||
|
- macos-arm
|
||||||
|
- windows-x86_64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- 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
|
||||||
- name: Get Rayhunter version
|
- name: Get Rayhunter version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
|
run: echo "VERSION=$(grep '^version' daemon/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
|
||||||
- name: Setup versioned release directory
|
- name: Setup versioned release directory
|
||||||
run: |
|
run: |
|
||||||
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
platform="${{ matrix.platform }}"
|
||||||
mkdir "$VERSIONED_DIR"
|
dest="rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}"
|
||||||
mv rayhunter-daemon-* rootshell/rootshell installer-* dist/* installer/install.ps1 "$VERSIONED_DIR"/
|
mkdir "$dest"
|
||||||
- name: Archive release directory as zip
|
mv installer-$platform/installer* "$dest"/installer
|
||||||
run: |
|
cp -r rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
|
||||||
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
zip -r "$dest.zip" "$dest"
|
||||||
zip -r "$VERSIONED_DIR.zip" "$VERSIONED_DIR"
|
sha256sum "$dest.zip" > "$dest.zip.sha256"
|
||||||
- name: Compute SHA256 of zip
|
|
||||||
run: |
|
|
||||||
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
|
||||||
sha256sum "$VERSIONED_DIR.zip" > "$VERSIONED_DIR.zip.sha256"
|
|
||||||
# TODO: have this create a release directly
|
|
||||||
- name: Upload zip release and sha256
|
- name: Upload zip release and sha256
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rayhunter-v${{ env.VERSION }}
|
name: rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
rayhunter-v${{ env.VERSION }}.zip
|
rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip
|
||||||
rayhunter-v${{ env.VERSION }}.zip.sha256
|
rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- 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 bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
defined_versions=$(find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
||||||
find lib bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
find lib check daemon installer 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
|
||||||
@@ -45,4 +45,4 @@ jobs:
|
|||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
version=$(grep ^version lib/Cargo.toml | cut -d' ' -f3 | tr -d '"')
|
version=$(grep ^version lib/Cargo.toml | cut -d' ' -f3 | tr -d '"')
|
||||||
gh release create --generate-notes -t "Rayhunter v$version" "v$version" rayhunter-v${version}/rayhunter-*
|
gh release create --generate-notes -t "Rayhunter v$version" "v$version" rayhunter-v${version}-*/rayhunter-v${version}*.zi*
|
||||||
|
|||||||
@@ -168,8 +168,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asn1-codecs"
|
name = "asn1-codecs"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eb148e3886345ed6c20c984393359bfda8220c82114ac5d12fcd54c0f393e91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitvec",
|
"bitvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -179,8 +180,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asn1-compiler"
|
name = "asn1-compiler"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a0ab1fb91f80ed8e751a8cb3d38dc822031dbfb769731e9d4a8bc184a39a230"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitvec",
|
"bitvec",
|
||||||
@@ -197,8 +199,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asn1_codecs_derive"
|
name = "asn1_codecs_derive"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "129a9f02b40e743766102dbbb93464b5868fb5e264a02a10d87fb45c472dd4b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"asn1-codecs",
|
"asn1-codecs",
|
||||||
"bitvec",
|
"bitvec",
|
||||||
@@ -306,7 +309,6 @@ checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
@@ -323,13 +325,11 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -349,7 +349,6 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1490,7 +1489,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "installer"
|
name = "installer"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adb_client",
|
"adb_client",
|
||||||
"aes",
|
"aes",
|
||||||
@@ -1503,7 +1502,8 @@ dependencies = [
|
|||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"md5",
|
"md5 0.7.0",
|
||||||
|
"md5crypt",
|
||||||
"nusb",
|
"nusb",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1745,6 +1745,21 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md5"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md5crypt"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8db1a978f99f584f2e72601860f68bb2cea9c09f4408f5e83f805db3434fb0"
|
||||||
|
dependencies = [
|
||||||
|
"md5 0.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdns-sd"
|
name = "mdns-sd"
|
||||||
version = "0.13.9"
|
version = "0.13.9"
|
||||||
@@ -1771,16 +1786,6 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mime_guess"
|
|
||||||
version = "2.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
|
||||||
dependencies = [
|
|
||||||
"mime",
|
|
||||||
"unicase",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1938,6 +1943,28 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_threads"
|
name = "num_threads"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -1987,29 +2014,6 @@ version = "2.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets 0.52.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -2220,6 +2224,19 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycrate-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/EFForg/pycrate-rs#9e72e40bee9c3c09205ad871cf681628b443de7c"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"deku",
|
||||||
|
"env_logger 0.11.8",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qoi"
|
name = "qoi"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2367,7 +2384,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2377,22 +2394,38 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix",
|
||||||
|
"num_enum",
|
||||||
"pcap-file-tokio",
|
"pcap-file-tokio",
|
||||||
|
"pycrate-rs",
|
||||||
"serde",
|
"serde",
|
||||||
"telcom-parser",
|
"telcom-parser",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayhunter-check"
|
||||||
|
version = "0.5.1"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"pcap-file-tokio",
|
||||||
|
"rayhunter",
|
||||||
|
"simple_logger",
|
||||||
|
"tokio",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -2400,11 +2433,9 @@ dependencies = [
|
|||||||
"include_dir",
|
"include_dir",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
|
||||||
"rayhunter",
|
"rayhunter",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simple_logger",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2433,15 +2464,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.9.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -2515,7 +2537,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rootshell"
|
name = "rootshell"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix",
|
"nix",
|
||||||
]
|
]
|
||||||
@@ -2603,6 +2625,15 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2888,7 +2919,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telcom-parser"
|
name = "telcom-parser"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"asn1-codecs",
|
"asn1-codecs",
|
||||||
"asn1-compiler",
|
"asn1-compiler",
|
||||||
@@ -3025,7 +3056,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
@@ -3151,7 +3181,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3172,7 +3201,6 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
@@ -3198,12 +3226,6 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicase"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -3262,6 +3284,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
members = [
|
members = [
|
||||||
"lib",
|
"lib",
|
||||||
"bin",
|
"daemon",
|
||||||
|
"check",
|
||||||
"rootshell",
|
"rootshell",
|
||||||
"telcom-parser",
|
"telcom-parser",
|
||||||
"installer",
|
"installer",
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
use clap::Parser;
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
use log::{info, warn};
|
|
||||||
use rayhunter::{
|
|
||||||
analysis::analyzer::{AnalyzerConfig, EventType, Harness},
|
|
||||||
diag::DataType,
|
|
||||||
gsmtap_parser,
|
|
||||||
pcap::GsmtapPcapWriter,
|
|
||||||
qmdl::QmdlReader,
|
|
||||||
};
|
|
||||||
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
|
||||||
use tokio::fs::{metadata, read_dir, File};
|
|
||||||
|
|
||||||
mod dummy_analyzer;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about)]
|
|
||||||
struct Args {
|
|
||||||
#[arg(short = 'p', long)]
|
|
||||||
qmdl_path: PathBuf,
|
|
||||||
|
|
||||||
#[arg(short = 'c', long)]
|
|
||||||
pcapify: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
show_skipped: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
enable_dummy_analyzer: bool,
|
|
||||||
|
|
||||||
#[arg(short, long)]
|
|
||||||
verbose: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) {
|
|
||||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
|
||||||
if enable_dummy_analyzer {
|
|
||||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
|
||||||
}
|
|
||||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
|
||||||
let file_size = qmdl_file
|
|
||||||
.metadata()
|
|
||||||
.await
|
|
||||||
.expect("failed to get QMDL file metadata")
|
|
||||||
.len();
|
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
|
||||||
let mut qmdl_stream = pin!(qmdl_reader
|
|
||||||
.as_stream()
|
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
|
||||||
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
|
|
||||||
let mut total_messages = 0;
|
|
||||||
let mut warnings = 0;
|
|
||||||
let mut skipped = 0;
|
|
||||||
while let Some(container) = qmdl_stream
|
|
||||||
.try_next()
|
|
||||||
.await
|
|
||||||
.expect("failed getting QMDL container")
|
|
||||||
{
|
|
||||||
let row = harness.analyze_qmdl_messages(container);
|
|
||||||
total_messages += 1;
|
|
||||||
for reason in row.skipped_message_reasons {
|
|
||||||
*skipped_reasons.entry(reason).or_insert(0) += 1;
|
|
||||||
skipped += 1;
|
|
||||||
}
|
|
||||||
for analysis in row.analysis {
|
|
||||||
for maybe_event in analysis.events {
|
|
||||||
let Some(event) = maybe_event else { continue };
|
|
||||||
match event.event_type {
|
|
||||||
EventType::Informational => {
|
|
||||||
info!(
|
|
||||||
"{}: INFO - {} {}",
|
|
||||||
qmdl_path, analysis.timestamp, event.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
EventType::QualitativeWarning { severity } => {
|
|
||||||
warn!(
|
|
||||||
"{}: WARNING (Severity: {:?}) - {} {}",
|
|
||||||
qmdl_path, severity, analysis.timestamp, event.message,
|
|
||||||
);
|
|
||||||
warnings += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if show_skipped && skipped > 0 {
|
|
||||||
info!("{}: messages skipped:", qmdl_path);
|
|
||||||
for (reason, count) in skipped_reasons.iter() {
|
|
||||||
info!(" - {}: \"{}\"", count, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!(
|
|
||||||
"{}: {} messages analyzed, {} warnings, {} messages skipped",
|
|
||||||
qmdl_path, total_messages, warnings, skipped
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pcapify(qmdl_path: &PathBuf) {
|
|
||||||
let qmdl_file = &mut File::open(&qmdl_path)
|
|
||||||
.await
|
|
||||||
.expect("failed to open qmdl file");
|
|
||||||
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
|
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
|
|
||||||
let mut pcap_path = qmdl_path.clone();
|
|
||||||
pcap_path.set_extension("pcap");
|
|
||||||
let pcap_file = &mut File::create(&pcap_path)
|
|
||||||
.await
|
|
||||||
.expect("failed to open pcap file");
|
|
||||||
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
|
||||||
pcap_writer.write_iface_header().await.unwrap();
|
|
||||||
while let Some(container) = qmdl_reader
|
|
||||||
.get_next_messages_container()
|
|
||||||
.await
|
|
||||||
.expect("failed to get container")
|
|
||||||
{
|
|
||||||
for msg in container.into_messages().into_iter().flatten() {
|
|
||||||
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
|
|
||||||
pcap_writer
|
|
||||||
.write_gsmtap_message(parsed, timestamp)
|
|
||||||
.await
|
|
||||||
.expect("failed to write");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("wrote pcap to {:?}", &pcap_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let args = Args::parse();
|
|
||||||
let level = if args.verbose {
|
|
||||||
log::LevelFilter::Trace
|
|
||||||
} else {
|
|
||||||
log::LevelFilter::Warn
|
|
||||||
};
|
|
||||||
simple_logger::SimpleLogger::new()
|
|
||||||
.with_colors(true)
|
|
||||||
.without_timestamps()
|
|
||||||
.with_level(level)
|
|
||||||
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
|
|
||||||
.with_module_level("asn1_codecs", log::LevelFilter::Error)
|
|
||||||
.init()
|
|
||||||
.unwrap();
|
|
||||||
info!("Analyzers:");
|
|
||||||
|
|
||||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
|
||||||
if args.enable_dummy_analyzer {
|
|
||||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
|
||||||
}
|
|
||||||
for analyzer in harness.get_metadata().analyzers {
|
|
||||||
info!(" - {}: {}", analyzer.name, analyzer.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = metadata(&args.qmdl_path)
|
|
||||||
.await
|
|
||||||
.expect("failed to get metadata");
|
|
||||||
if metadata.is_dir() {
|
|
||||||
let mut dir = read_dir(&args.qmdl_path).await.expect("failed to read dir");
|
|
||||||
while let Some(entry) = dir.next_entry().await.expect("failed to get entry") {
|
|
||||||
let name = entry.file_name();
|
|
||||||
let name_str = name.to_str().unwrap();
|
|
||||||
if name_str.ends_with(".qmdl") {
|
|
||||||
let path = entry.path();
|
|
||||||
let path_str = path.to_str().unwrap();
|
|
||||||
analyze_file(args.enable_dummy_analyzer, path_str, args.show_skipped).await;
|
|
||||||
if args.pcapify {
|
|
||||||
pcapify(&path).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let path = args.qmdl_path.to_str().unwrap();
|
|
||||||
analyze_file(args.enable_dummy_analyzer, path, args.show_skipped).await;
|
|
||||||
if args.pcapify {
|
|
||||||
pcapify(&args.qmdl_path).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
use std::pin::pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use axum::body::Body;
|
|
||||||
use axum::extract::{Path, State};
|
|
||||||
use axum::http::header::CONTENT_TYPE;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use futures::{StreamExt, TryStreamExt};
|
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
|
||||||
use rayhunter::diag::DataType;
|
|
||||||
use rayhunter::diag_device::DiagDevice;
|
|
||||||
use rayhunter::qmdl::QmdlWriter;
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tokio_util::io::ReaderStream;
|
|
||||||
use tokio_util::task::TaskTracker;
|
|
||||||
|
|
||||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
|
||||||
use crate::display;
|
|
||||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
|
||||||
use crate::server::ServerState;
|
|
||||||
|
|
||||||
pub enum DiagDeviceCtrlMessage {
|
|
||||||
StopRecording,
|
|
||||||
StartRecording,
|
|
||||||
Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_diag_read_thread(
|
|
||||||
task_tracker: &TaskTracker,
|
|
||||||
mut dev: DiagDevice,
|
|
||||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
|
||||||
ui_update_sender: Sender<display::DisplayState>,
|
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
|
||||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
|
||||||
enable_dummy_analyzer: bool,
|
|
||||||
analyzer_config: AnalyzerConfig,
|
|
||||||
) {
|
|
||||||
task_tracker.spawn(async move {
|
|
||||||
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
|
||||||
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
|
||||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
|
||||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await
|
|
||||||
.expect("failed to create analysis writer"));
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
msg = qmdl_file_rx.recv() => {
|
|
||||||
match msg {
|
|
||||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("couldn't create new qmdl entry: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
|
|
||||||
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
|
||||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await
|
|
||||||
.expect("failed to write to analysis file"));
|
|
||||||
|
|
||||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {
|
|
||||||
warn!("couldn't send ui update message: {}", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
|
||||||
if let Err(e) = analysis_sender
|
|
||||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
|
||||||
entry.name.to_string(),
|
|
||||||
))
|
|
||||||
.await {
|
|
||||||
warn!("couldn't send analysis message: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(e) = qmdl_store.close_current_entry().await {
|
|
||||||
error!("couldn't close current entry: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_qmdl_writer = None;
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
|
||||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
|
||||||
}
|
|
||||||
maybe_analysis_writer = None;
|
|
||||||
|
|
||||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Paused).await {
|
|
||||||
warn!("couldn't send ui update message: {}", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// None means all the Senders have been dropped, so it's
|
|
||||||
// time to go
|
|
||||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
|
||||||
info!("Diag reader thread exiting...");
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
|
||||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
|
||||||
}
|
|
||||||
return Ok(())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maybe_container = diag_stream.next() => {
|
|
||||||
match maybe_container.unwrap() {
|
|
||||||
Ok(container) => {
|
|
||||||
if container.data_type != DataType::UserSpace {
|
|
||||||
debug!("skipping non-userspace diag messages...");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// keep track of how many bytes were written to the QMDL file so we can read
|
|
||||||
// a valid block of data from it in the HTTP server
|
|
||||||
if let Some(qmdl_writer) = maybe_qmdl_writer.as_mut() {
|
|
||||||
qmdl_writer.write_container(&container).await.expect("failed to write to QMDL writer");
|
|
||||||
debug!("total QMDL bytes written: {}, updating manifest...", qmdl_writer.total_written);
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
|
||||||
qmdl_store.update_entry_qmdl_size(index, qmdl_writer.total_written).await
|
|
||||||
.expect("failed to update qmdl file size");
|
|
||||||
debug!("done!");
|
|
||||||
} else {
|
|
||||||
debug!("no qmdl_writer set, continuing...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
|
||||||
let analysis_output = analysis_writer.analyze(container).await
|
|
||||||
.expect("failed to analyze container");
|
|
||||||
let (analysis_file_len, heuristic_warning) = analysis_output;
|
|
||||||
if heuristic_warning {
|
|
||||||
info!("a heuristic triggered on this run!");
|
|
||||||
ui_update_sender.send(display::DisplayState::WarningDetected).await
|
|
||||||
.expect("couldn't send ui update message: {}");
|
|
||||||
}
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
|
||||||
qmdl_store.update_entry_analysis_size(index, analysis_file_len).await
|
|
||||||
.expect("failed to update analysis file size");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("error reading diag device: {}", err);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_recording(
|
|
||||||
State(state): State<Arc<ServerState>>,
|
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
|
||||||
if state.config.debug_mode {
|
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
state
|
|
||||||
.diag_device_ctrl_sender
|
|
||||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send start recording message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn stop_recording(
|
|
||||||
State(state): State<Arc<ServerState>>,
|
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
|
||||||
if state.config.debug_mode {
|
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
|
||||||
}
|
|
||||||
state
|
|
||||||
.diag_device_ctrl_sender
|
|
||||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send stop recording message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_recording(
|
|
||||||
State(state): State<Arc<ServerState>>,
|
|
||||||
Path(qmdl_name): Path<String>,
|
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
|
||||||
if state.config.debug_mode {
|
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
|
||||||
}
|
|
||||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
|
||||||
match qmdl_store.delete_entry(&qmdl_name).await {
|
|
||||||
Err(RecordingStoreError::NoSuchEntryError) => {
|
|
||||||
return Err((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
format!("no recording with name {qmdl_name}"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err((
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't delete recording: {e}"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Ok(_) => {}
|
|
||||||
}
|
|
||||||
state
|
|
||||||
.diag_device_ctrl_sender
|
|
||||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send stop recording message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
state
|
|
||||||
.ui_update_sender
|
|
||||||
.send(display::DisplayState::Paused)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send ui update message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_all_recordings(
|
|
||||||
State(state): State<Arc<ServerState>>,
|
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
|
||||||
if state.config.debug_mode {
|
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
|
||||||
}
|
|
||||||
state
|
|
||||||
.diag_device_ctrl_sender
|
|
||||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send stop recording message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
|
||||||
qmdl_store.delete_all_entries().await.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't delete all recordings: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
state
|
|
||||||
.ui_update_sender
|
|
||||||
.send(display::DisplayState::Paused)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send ui update message: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_analysis_report(
|
|
||||||
State(state): State<Arc<ServerState>>,
|
|
||||||
Path(qmdl_name): Path<String>,
|
|
||||||
) -> Result<Response, (StatusCode, String)> {
|
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
|
||||||
let (entry_index, _) = if qmdl_name == "live" {
|
|
||||||
qmdl_store.get_current_entry().ok_or((
|
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
|
||||||
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string(),
|
|
||||||
))?
|
|
||||||
} else {
|
|
||||||
qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
format!("Couldn't find QMDL entry with name \"{}\"", qmdl_name),
|
|
||||||
))?
|
|
||||||
};
|
|
||||||
let analysis_file = qmdl_store
|
|
||||||
.open_entry_analysis(entry_index)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
|
||||||
let analysis_stream = ReaderStream::new(analysis_file);
|
|
||||||
|
|
||||||
let headers = [(CONTENT_TYPE, "application/x-ndjson")];
|
|
||||||
let body = Body::from_stream(analysis_stream);
|
|
||||||
Ok((headers, body).into_response())
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
mod generic_framebuffer;
|
|
||||||
|
|
||||||
#[cfg(feature = "tplink")]
|
|
||||||
mod tplink;
|
|
||||||
#[cfg(feature = "tplink")]
|
|
||||||
mod tplink_framebuffer;
|
|
||||||
#[cfg(feature = "tplink")]
|
|
||||||
mod tplink_onebit;
|
|
||||||
|
|
||||||
#[cfg(feature = "tplink")]
|
|
||||||
pub use tplink::update_ui;
|
|
||||||
|
|
||||||
#[cfg(feature = "orbic")]
|
|
||||||
mod orbic;
|
|
||||||
#[cfg(feature = "orbic")]
|
|
||||||
pub use orbic::update_ui;
|
|
||||||
|
|
||||||
#[cfg(feature = "wingtech")]
|
|
||||||
mod wingtech;
|
|
||||||
#[cfg(feature = "wingtech")]
|
|
||||||
pub use wingtech::update_ui;
|
|
||||||
|
|
||||||
pub enum DisplayState {
|
|
||||||
Recording,
|
|
||||||
Paused,
|
|
||||||
WarningDetected,
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity};
|
|
||||||
|
|
||||||
use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity};
|
|
||||||
use rayhunter::analysis::information_element::{InformationElement, LteInformationElement};
|
|
||||||
|
|
||||||
pub struct TestAnalyzer {
|
|
||||||
pub count: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Analyzer for TestAnalyzer {
|
|
||||||
fn get_name(&self) -> Cow<str> {
|
|
||||||
Cow::from("Example Analyzer")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_description(&self) -> Cow<str> {
|
|
||||||
Cow::from("Always returns true, if you are seeing this you are either a developer or you are about to have problems.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
|
||||||
self.count += 1;
|
|
||||||
if self.count % 100 == 0 {
|
|
||||||
return Some(Event {
|
|
||||||
event_type: EventType::Informational,
|
|
||||||
message: "multiple of 100 events processed".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let pcch_msg = match ie {
|
|
||||||
InformationElement::LTE(lte_ie) => match &**lte_ie {
|
|
||||||
LteInformationElement::PCCH(pcch_msg) => pcch_msg,
|
|
||||||
_ => return None,
|
|
||||||
},
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
for record in &paging.paging_record_list.as_ref()?.0 {
|
|
||||||
if let PagingUE_Identity::S_TMSI(_) = record.ue_identity {
|
|
||||||
return Some(Event {
|
|
||||||
event_type: EventType::QualitativeWarning {
|
|
||||||
severity: Severity::Low,
|
|
||||||
},
|
|
||||||
message: "TMSI was provided to cell".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": [
|
|
||||||
"prettier-plugin-svelte"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.svelte",
|
|
||||||
"options": {
|
|
||||||
"parser": "svelte"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import prettier from "eslint-config-prettier";
|
|
||||||
import js from '@eslint/js';
|
|
||||||
import svelte from 'eslint-plugin-svelte';
|
|
||||||
import globals from 'globals';
|
|
||||||
import ts from 'typescript-eslint';
|
|
||||||
|
|
||||||
export default ts.config(
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: ["build/", ".svelte-kit/", "dist/"]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "web",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"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",
|
|
||||||
"test:unit": "vitest",
|
|
||||||
"test": "npm run test:unit -- --run",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint": "prettier --check . && eslint ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
|
||||||
"@sveltejs/adapter-static": "^3.0.5",
|
|
||||||
"@sveltejs/kit": "^2.0.0",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
||||||
"@types/eslint": "^9.6.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"eslint": "^9.7.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
|
||||||
"globals": "^15.0.0",
|
|
||||||
"prettier": "^3.3.2",
|
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
|
||||||
"svelte": "^5.0.0",
|
|
||||||
"svelte-check": "^4.0.0",
|
|
||||||
"tailwindcss": "^3.4.9",
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"typescript-eslint": "^8.0.0",
|
|
||||||
"vite": "^5.0.3",
|
|
||||||
"vitest": "^2.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@import "tailwindcss/base";
|
|
||||||
@import "tailwindcss/components";
|
|
||||||
@import "tailwindcss/utilities"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// See https://svelte.dev/docs/kit/types#app
|
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
|
||||||
namespace App {
|
|
||||||
// interface Error {}
|
|
||||||
// interface Locals {}
|
|
||||||
// interface PageData {}
|
|
||||||
// interface PageState {}
|
|
||||||
// interface Platform {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" data-theme="dark">
|
|
||||||
<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" class="m-4 xl:m-8">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { EventType, parse_finished_report, Severity, type QualitativeWarning } from './analysis.svelte';
|
|
||||||
import { parse_ndjson, type NewlineDeliminatedJson } from './ndjson';
|
|
||||||
|
|
||||||
const SAMPLE_REPORT_NDJSON: NewlineDeliminatedJson = [
|
|
||||||
{ "analyzers": [{ "name": "LTE SIB 6/7 Downgrade", "description": "Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities." }, { "name": "IMSI Provided", "description": "Tests whether the UE's IMSI was ever provided to the cell" }, { "name": "Null Cipher", "description": "Tests whether the cell suggests using a null cipher (EEA0)" }, { "name": "Example Analyzer", "description": "Always returns true, if you are seeing this you are either a developer or you are about to have problems." }] },
|
|
||||||
{ "timestamp": "2024-10-08T13:25:43.011689003-07:00", "skipped_message_reasons": ["DecodingError(UperDecodeError(Error { cause: BufferTooShort, msg: \"PerCodec:DecodeError:Requested Bits to decode 3, Remaining bits 1\", context: [] }))"], "analysis": [] },
|
|
||||||
{ "timestamp": "2024-10-08T13:25:43.480872496-07:00", "skipped_message_reasons": [], "analysis": [{ "timestamp": "2024-08-19T03:33:54.318Z", "events": [null, null, null, { "event_type": { "type": "QualitativeWarning", "severity": "Low" }, "message": "TMSI was provided to cell" }] }] },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('analysis report parsing', () => {
|
|
||||||
it('parses the example analysis', () => {
|
|
||||||
const report = parse_finished_report(SAMPLE_REPORT_NDJSON);
|
|
||||||
expect(report.metadata.analyzers).toEqual([
|
|
||||||
{
|
|
||||||
"name":"LTE SIB 6/7 Downgrade",
|
|
||||||
"description":"Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"IMSI Provided",
|
|
||||||
"description":"Tests whether the UE's IMSI was ever provided to the cell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Null Cipher",
|
|
||||||
"description":"Tests whether the cell suggests using a null cipher (EEA0)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Example Analyzer",
|
|
||||||
"description":"Always returns true, if you are seeing this you are either a developer or you are about to have problems.",
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
expect(report.rows).toHaveLength(2);
|
|
||||||
expect(report.rows[0].skipped_message_reasons).toHaveLength(1);
|
|
||||||
expect(report.rows[0].analysis).toHaveLength(0);
|
|
||||||
expect(report.rows[1].skipped_message_reasons).toHaveLength(0);
|
|
||||||
expect(report.rows[1].analysis).toHaveLength(1);
|
|
||||||
expect(report.rows[1].analysis[0].events).toHaveLength(1);
|
|
||||||
const event = report.rows[1].analysis[0].events[0];
|
|
||||||
if (event.type === EventType.Warning) {
|
|
||||||
expect(event.severity).toEqual(Severity.Low);
|
|
||||||
} else {
|
|
||||||
throw 'wrong event type';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { parse_ndjson, type NewlineDeliminatedJson } from "./ndjson";
|
|
||||||
import { req } from "./utils.svelte";
|
|
||||||
|
|
||||||
export type AnalysisReport = {
|
|
||||||
metadata: ReportMetadata;
|
|
||||||
rows: AnalysisRow[];
|
|
||||||
statistics: ReportStatistics;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReportStatistics = {
|
|
||||||
num_warnings: number;
|
|
||||||
num_informational_logs: number;
|
|
||||||
num_skipped_packets: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ReportMetadata = {
|
|
||||||
analyzers: AnalyzerMetadata[];
|
|
||||||
rayhunter: RayhunterMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RayhunterMetadata = {
|
|
||||||
rayhunter_version: string;
|
|
||||||
system_os: string;
|
|
||||||
arch: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyzerMetadata = {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalysisRow = {
|
|
||||||
timestamp: Date;
|
|
||||||
skipped_message_reasons: string[];
|
|
||||||
analysis: PacketAnalysis[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PacketAnalysis = {
|
|
||||||
timestamp: Date;
|
|
||||||
events: Event[];
|
|
||||||
};
|
|
||||||
export type Event = QualitativeWarning | InformationalEvent;
|
|
||||||
export enum EventType {
|
|
||||||
Informational,
|
|
||||||
Warning,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QualitativeWarning = {
|
|
||||||
type: EventType.Warning;
|
|
||||||
severity: Severity;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum Severity {
|
|
||||||
Low,
|
|
||||||
Medium,
|
|
||||||
High,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InformationalEvent = {
|
|
||||||
type: EventType.Informational;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport {
|
|
||||||
const metadata: ReportMetadata = report_json[0]; // this can be cast directly
|
|
||||||
let num_warnings = 0;
|
|
||||||
let num_informational_logs = 0;
|
|
||||||
let num_skipped_packets = 0;
|
|
||||||
const rows: AnalysisRow[] = report_json.slice(1).map((row_json: any) => {
|
|
||||||
const analysis: PacketAnalysis[] = row_json.analysis.map((analysis_json: any) => {
|
|
||||||
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
|
||||||
if (event_json === null) {
|
|
||||||
return null;
|
|
||||||
} else if (event_json.event_type.type === "Informational") {
|
|
||||||
num_informational_logs += 1;
|
|
||||||
return {
|
|
||||||
type: EventType.Informational,
|
|
||||||
message: event_json.message,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
num_warnings += 1;
|
|
||||||
return {
|
|
||||||
type: EventType.Warning,
|
|
||||||
severity: event_json.event_type.severity === "High" ? Severity.High :
|
|
||||||
event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
|
|
||||||
message: event_json.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((maybe_event: Event | null) => maybe_event !== null);
|
|
||||||
return {
|
|
||||||
timestamp: analysis_json.timestamp,
|
|
||||||
events,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
num_skipped_packets += row_json.skipped_message_reasons.length;
|
|
||||||
return {
|
|
||||||
timestamp: new Date(row_json.timestamp),
|
|
||||||
skipped_message_reasons: row_json.skipped_message_reasons,
|
|
||||||
analysis,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
statistics: {
|
|
||||||
num_informational_logs,
|
|
||||||
num_warnings,
|
|
||||||
num_skipped_packets,
|
|
||||||
},
|
|
||||||
metadata,
|
|
||||||
rows,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get_report(name: string): Promise<AnalysisReport> {
|
|
||||||
const report_json = parse_ndjson(await req('GET', `/api/analysis-report/${name}`));
|
|
||||||
return parse_finished_report(report_json);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
|
||||||
import { EventType } from "$lib/analysis.svelte";
|
|
||||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
let { entry, onclick, analysis_visible}: {
|
|
||||||
entry: ManifestEntry,
|
|
||||||
onclick: () => void,
|
|
||||||
analysis_visible: boolean,
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let summary = $derived.by(() => {
|
|
||||||
if (entry.analysis_status === AnalysisStatus.Queued) {
|
|
||||||
return 'Queued...';
|
|
||||||
} else if (entry.analysis_status === AnalysisStatus.Running) {
|
|
||||||
return 'Running...';
|
|
||||||
} else if (entry.analysis_status === AnalysisStatus.Finished) {
|
|
||||||
if (entry.analysis_report === undefined) {
|
|
||||||
return 'Loading...';
|
|
||||||
} else if (typeof(entry.analysis_report) === 'string') {
|
|
||||||
return entry.analysis_report;
|
|
||||||
} else {
|
|
||||||
let num_warnings = 0;
|
|
||||||
for (let row of entry.analysis_report.rows) {
|
|
||||||
for (let analysis of row.analysis) {
|
|
||||||
for (let event of analysis.events) {
|
|
||||||
if (event.type === EventType.Warning) {
|
|
||||||
num_warnings += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${num_warnings} warnings`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return 'Loading...';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let ready = $derived.by(() => {
|
|
||||||
let finished = entry.analysis_status === AnalysisStatus.Finished;
|
|
||||||
let report_available = entry.analysis_report !== undefined;
|
|
||||||
return finished && report_available;
|
|
||||||
})
|
|
||||||
|
|
||||||
let button_class = $derived(ready ? "text-blue-600 border rounded-full px-2" : '');
|
|
||||||
</script>
|
|
||||||
<button class="flex flex-row gap-1 lg:gap-2" disabled={!ready} {onclick}>
|
|
||||||
<span class="{button_class} {entry.get_num_warnings() < 1 ? 'text-green-700 border-green-500 bg-green-200' : 'text-red-700 border-red-500 bg-red-200'}">{summary}</span>
|
|
||||||
<svg class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}" 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="m19 9-7 7-7-7"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
|
||||||
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow, type AnalysisReport } from "$lib/analysis.svelte";
|
|
||||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
let { report }: {
|
|
||||||
report: AnalysisReport,
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
|
||||||
timeStyle: "long",
|
|
||||||
dateStyle: "short",
|
|
||||||
});
|
|
||||||
|
|
||||||
const skipped_messages: Map<string, number> = $derived.by(() => {
|
|
||||||
let map = new Map();
|
|
||||||
for (const row of report.rows) {
|
|
||||||
for (const message of row.skipped_message_reasons) {
|
|
||||||
let count = map.get(message);
|
|
||||||
if (count === undefined) {
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
map.set(message, count + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<div>
|
|
||||||
<p class="text-lg underline">Warnings and Informational Logs</p>
|
|
||||||
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
|
||||||
<p>Nothing to show!</p>
|
|
||||||
{:else}
|
|
||||||
<table class="table-auto text-left">
|
|
||||||
<thead class="p-2">
|
|
||||||
<tr class="bg-gray-300">
|
|
||||||
<th class="p-2">Timestamp</th>
|
|
||||||
<th class="p-2">Warning</th>
|
|
||||||
<th class="p-2">Severity</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each report.rows as row, row_idx}
|
|
||||||
{#each row.analysis as analysis}
|
|
||||||
{@const parsed_date = new Date(analysis.timestamp)}
|
|
||||||
{#each analysis.events.filter(e => e !== null) as event}
|
|
||||||
<tr class="even:bg-gray-200 odd:bg-white">
|
|
||||||
{#if event.type === EventType.Warning}
|
|
||||||
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
|
||||||
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
|
|
||||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
|
||||||
<td class="p-2">{event.message}</td>
|
|
||||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
|
||||||
{:else if event.type === EventType.Informational}
|
|
||||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
|
||||||
<td class="p-2">{event.message}</td>
|
|
||||||
<td class="p-2">Info</td>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if report.statistics.num_skipped_packets > 0}
|
|
||||||
<div>
|
|
||||||
<p class="text-lg underline">Unparsed Messages</p>
|
|
||||||
<p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p>
|
|
||||||
<table class="table-auto text-left">
|
|
||||||
<thead class="p-2">
|
|
||||||
<tr class="bg-gray-300">
|
|
||||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
|
||||||
<th scope="col">Reason/Error</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each skipped_messages.entries() as [message, count]}
|
|
||||||
<tr class="even:bg-gray-200 odd:bg-white">
|
|
||||||
<td class="text-center">{count}</td>
|
|
||||||
<td>{message}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
|
||||||
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow } from "$lib/analysis.svelte";
|
|
||||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
import AnalysisTable from "./AnalysisTable.svelte";
|
|
||||||
let { entry }: {
|
|
||||||
entry: ManifestEntry,
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
|
||||||
timeStyle: "long",
|
|
||||||
dateStyle: "short",
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container mt-2">
|
|
||||||
{#if entry.analysis_report === undefined}
|
|
||||||
<p>Report unavailable, try refreshing.</p>
|
|
||||||
{:else if typeof(entry.analysis_report) === 'string'}
|
|
||||||
<p>Error getting analysis report: {entry.analysis_report}</p>
|
|
||||||
{:else}
|
|
||||||
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#if entry.analysis_report.rows.length > 0}
|
|
||||||
<AnalysisTable report={entry.analysis_report} />
|
|
||||||
{:else}
|
|
||||||
<p>No warnings to display!</p>
|
|
||||||
{/if}
|
|
||||||
{#if metadata !== undefined && metadata.rayhunter !== undefined}
|
|
||||||
<div>
|
|
||||||
<p class="text-lg underline">Metadata</p>
|
|
||||||
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
|
||||||
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-lg underline">Analyzers</p>
|
|
||||||
{#each metadata.analyzers as analyzer}
|
|
||||||
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<p>N/A (analysis generated by an older version of rayhunter)</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { req } from "$lib/utils.svelte";
|
|
||||||
import DeleteButton from "./DeleteButton.svelte";
|
|
||||||
|
|
||||||
function confirmDelete() {
|
|
||||||
if (window.confirm(`Permanently delete ALL recordings?`)) {
|
|
||||||
req('POST', '/api/delete-all-recordings')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-end gap-2">
|
|
||||||
<DeleteButton
|
|
||||||
text="Delete ALL Recordings"
|
|
||||||
prompt={`Are you sure you want to delete ALL recordings?`}
|
|
||||||
url={`/api/delete-all-recordings`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
import { req } from "$lib/utils.svelte";
|
|
||||||
let { text, url, prompt }: {
|
|
||||||
text?: string,
|
|
||||||
url: string,
|
|
||||||
prompt: string,
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
function confirmDelete() {
|
|
||||||
if (window.confirm(prompt)) {
|
|
||||||
req('POST', url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row" onclick={confirmDelete} aria-label="delete">
|
|
||||||
<p>{text}</p>
|
|
||||||
<svg
|
|
||||||
style="width:24px;height:24px"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="white"
|
|
||||||
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { url, text, full_button=false }: {
|
|
||||||
url: string;
|
|
||||||
text: string;
|
|
||||||
full_button?: boolean;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
function download() {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="flex flex-row {full_button ? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md' : 'text-blue-600 underline'}" onclick={download}>
|
|
||||||
{text}
|
|
||||||
<svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
||||||
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
import TableRow from "./ManifestTableRow.svelte";
|
|
||||||
import Card from "./ManifestCard.svelte"
|
|
||||||
interface Props {
|
|
||||||
entries: ManifestEntry[];
|
|
||||||
server_is_recording: boolean;
|
|
||||||
}
|
|
||||||
let { entries, server_is_recording }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!--For larger screens we use a table-->
|
|
||||||
<table class="hidden table-auto text-left lg:table">
|
|
||||||
<thead>
|
|
||||||
<tr class="bg-gray-100 drop-shadow">
|
|
||||||
<th class='p-2' scope="col">ID</th>
|
|
||||||
<th class='p-2' scope="col">Started</th>
|
|
||||||
<th class='p-2' scope="col">Last Message</th>
|
|
||||||
<th class='p-2' scope="col">Size</th>
|
|
||||||
<th class='p-2' scope="col">PCAP</th>
|
|
||||||
<th class='p-2' scope="col">QMDL</th>
|
|
||||||
<th class='p-2' scope="col">ZIP</th>
|
|
||||||
<th class='p-2' scope="col">Analysis</th>
|
|
||||||
<th class='p-2' scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each entries as entry, i}
|
|
||||||
<TableRow {entry} current={false} {i} />
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--For smaller screens we use cards-->
|
|
||||||
<div class="lg:hidden flex flex-col gap-4">
|
|
||||||
{#each entries as entry, i}
|
|
||||||
<Card {entry} current={false} {i} {server_is_recording} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { req } from "$lib/utils.svelte";
|
|
||||||
let { server_is_recording }: {
|
|
||||||
server_is_recording: boolean;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let client_set_recording = $state(server_is_recording);
|
|
||||||
let waiting_for_server = $derived(client_set_recording !== server_is_recording);
|
|
||||||
|
|
||||||
async function start_recording() {
|
|
||||||
await req('POST', '/api/start-recording');
|
|
||||||
client_set_recording = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stop_recording() {
|
|
||||||
await req('POST', '/api/stop-recording');
|
|
||||||
client_set_recording = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recording_button_classes = "text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if waiting_for_server}
|
|
||||||
<button class={server_is_recording ? stop_recording_classes : start_recording_classes}>
|
|
||||||
{server_is_recording ? "Stopping..." : "Starting..."}
|
|
||||||
</button>
|
|
||||||
{:else if server_is_recording}
|
|
||||||
<button class="{recording_button_classes} bg-red-500 hover:bg-red-700" onclick={stop_recording}>
|
|
||||||
<span>Stop</span>
|
|
||||||
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button class="{recording_button_classes} bg-blue-500 hover:bg-blue-700" onclick={start_recording}>
|
|
||||||
<span>Start</span>
|
|
||||||
<svg class="w-6 h-6 text-white" 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="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export interface SystemStats {
|
|
||||||
disk_stats: DiskStats;
|
|
||||||
memory_stats: MemoryStats;
|
|
||||||
runtime_metadata: RuntimeMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeMetadata {
|
|
||||||
rayhunter_version: string,
|
|
||||||
system_os: string,
|
|
||||||
arch: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiskStats {
|
|
||||||
partition: string,
|
|
||||||
total_size: string,
|
|
||||||
used_size: string,
|
|
||||||
available_size: string,
|
|
||||||
used_percent: string,
|
|
||||||
mounted_on: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemoryStats {
|
|
||||||
total: string,
|
|
||||||
used: string,
|
|
||||||
free: string,
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import '../app.css';
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{@render children()}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
|
||||||
import { get_manifest, get_system_stats } from "$lib/utils.svelte";
|
|
||||||
import ManifestTable from "$lib/components/ManifestTable.svelte";
|
|
||||||
import Card from "$lib/components/ManifestCard.svelte";
|
|
||||||
import type { SystemStats } from "$lib/systemStats";
|
|
||||||
import { AnalysisManager } from "$lib/analysisManager.svelte";
|
|
||||||
import SystemStatsTable from "$lib/components/SystemStatsTable.svelte";
|
|
||||||
import DeleteAllButton from "$lib/components/DeleteAllButton.svelte";
|
|
||||||
import RecordingControls from "$lib/components//RecordingControls.svelte";
|
|
||||||
import ConfigForm from "$lib/components/ConfigForm.svelte";
|
|
||||||
|
|
||||||
let manager: AnalysisManager = new AnalysisManager();
|
|
||||||
let loaded = $state(false);
|
|
||||||
let recording = $state(false);
|
|
||||||
let entries: ManifestEntry[] = $state([]);
|
|
||||||
let current_entry: ManifestEntry | undefined = $state(undefined);
|
|
||||||
let system_stats: SystemStats | undefined = $state(undefined);
|
|
||||||
$effect(() => {
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
await manager.update();
|
|
||||||
let new_manifest = await get_manifest();
|
|
||||||
await new_manifest.set_analysis_status(manager);
|
|
||||||
entries = new_manifest.entries;
|
|
||||||
current_entry = new_manifest.current_entry;
|
|
||||||
recording = current_entry !== undefined;
|
|
||||||
|
|
||||||
system_stats = await get_system_stats();
|
|
||||||
loaded = true;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
|
|
||||||
<img src="/rayhunter_text.png" 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>
|
|
||||||
<div class="m-4 xl:mx-8 flex flex-col gap-4">
|
|
||||||
{#if loaded}
|
|
||||||
<div class="flex flex-col lg:flex-row gap-4">
|
|
||||||
{#if recording}
|
|
||||||
<Card entry={current_entry} current={true} i={0} server_is_recording={recording}/>
|
|
||||||
{:else}
|
|
||||||
<div class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 justify-between">
|
|
||||||
<span class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600">
|
|
||||||
<svg class="w-8 h-8 text-red-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
WARNING: Not Running
|
|
||||||
</span>
|
|
||||||
<span>Rayhunter is not currently running and will not detect abnormal behavior!</span>
|
|
||||||
<div class="flex flex-row justify-end mt-2">
|
|
||||||
<RecordingControls {recording} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<SystemStatsTable stats={system_stats!} />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<span class="text-xl">History</span>
|
|
||||||
<ManifestTable entries={entries} server_is_recording={recording} />
|
|
||||||
</div>
|
|
||||||
<DeleteAllButton/>
|
|
||||||
<ConfigForm />
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col justify-center items-center">
|
|
||||||
<img src="/rayhunter_orca_only.png" class="h-48 animate-spin"/>
|
|
||||||
<p class="text-xl">Loading...</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
kit: {
|
|
||||||
adapter: adapter({
|
|
||||||
// default options are shown. On some platforms
|
|
||||||
// these options are set automatically — see below
|
|
||||||
pages: 'build',
|
|
||||||
assets: 'build',
|
|
||||||
fallback: undefined,
|
|
||||||
precompress: false,
|
|
||||||
strict: true
|
|
||||||
}),
|
|
||||||
version: {
|
|
||||||
// Use a deterministic version string for reproducible builds.
|
|
||||||
// Without this option, SvelteKit will use a timestamp.
|
|
||||||
name: process.env.GITHUB_SHA || 'dev'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { Config } from 'tailwindcss';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
'rayhunter-blue': '#4e4eb1',
|
|
||||||
'rayhunter-dark-blue': '#3f3da0',
|
|
||||||
'rayhunter-green': '#94ea18'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: []
|
|
||||||
} as Config;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
}
|
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
|
||||||
// except $lib which is handled by https://svelte.dev/docs/kit/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,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "rayhunter-check"
|
||||||
|
version = "0.5.1"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rayhunter = { path = "../lib" }
|
||||||
|
futures = { version = "0.3.30", default-features = false }
|
||||||
|
log = "0.4.20"
|
||||||
|
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||||
|
pcap-file-tokio = "0.1.0"
|
||||||
|
clap = { version = "4.5.2", features = ["derive"] }
|
||||||
|
simple_logger = "5.0.0"
|
||||||
|
walkdir = "2.5.0"
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
||||||
|
use rayhunter::{
|
||||||
|
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
|
||||||
|
diag::DataType,
|
||||||
|
gsmtap_parser,
|
||||||
|
pcap::GsmtapPcapWriter,
|
||||||
|
qmdl::QmdlReader,
|
||||||
|
};
|
||||||
|
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||||
|
use tokio::fs::File;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'P', long)]
|
||||||
|
pcapify: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
show_skipped: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Report {
|
||||||
|
skipped_reasons: HashMap<String, u32>,
|
||||||
|
total_messages: u32,
|
||||||
|
warnings: u32,
|
||||||
|
skipped: u32,
|
||||||
|
file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
fn new(file_path: &str) -> Self {
|
||||||
|
Report {
|
||||||
|
file_path: file_path.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_row(&mut self, row: AnalysisRow) {
|
||||||
|
self.total_messages += 1;
|
||||||
|
if let Some(reason) = row.skipped_message_reason {
|
||||||
|
*self.skipped_reasons.entry(reason).or_insert(0) += 1;
|
||||||
|
self.skipped += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for maybe_event in row.events {
|
||||||
|
let Some(event) = maybe_event else { continue };
|
||||||
|
let Some(timestamp) = row.packet_timestamp else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match event.event_type {
|
||||||
|
EventType::Informational => {
|
||||||
|
info!("{}: INFO - {} {}", self.file_path, timestamp, event.message,);
|
||||||
|
}
|
||||||
|
EventType::QualitativeWarning { severity } => {
|
||||||
|
warn!(
|
||||||
|
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||||
|
self.file_path, severity, timestamp, event.message,
|
||||||
|
);
|
||||||
|
self.warnings += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_summary(&self, show_skipped: bool) {
|
||||||
|
if show_skipped && self.skipped > 0 {
|
||||||
|
info!("{}: messages skipped:", self.file_path);
|
||||||
|
for (reason, count) in self.skipped_reasons.iter() {
|
||||||
|
info!(" - {count}: \"{reason}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"{}: {} messages analyzed, {} warnings, {} messages skipped",
|
||||||
|
self.file_path, self.total_messages, self.warnings, self.skipped
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
|
||||||
|
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||||
|
let pcap_file = &mut File::open(&pcap_path).await.expect("failed to open file");
|
||||||
|
let mut pcap_reader = PcapNgReader::new(pcap_file)
|
||||||
|
.await
|
||||||
|
.expect("failed to read PCAP file");
|
||||||
|
let mut report = Report::new(pcap_path);
|
||||||
|
while let Some(Ok(block)) = pcap_reader.next_block().await {
|
||||||
|
let row = match block {
|
||||||
|
Block::EnhancedPacket(packet) => harness.analyze_pcap_packet(packet),
|
||||||
|
other => {
|
||||||
|
debug!("{pcap_path}: skipping pcap packet {other:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
report.process_row(row);
|
||||||
|
}
|
||||||
|
report.print_summary(show_skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
|
||||||
|
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||||
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
|
let file_size = qmdl_file
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.expect("failed to get QMDL file metadata")
|
||||||
|
.len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
|
let mut qmdl_stream = pin!(
|
||||||
|
qmdl_reader
|
||||||
|
.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
|
||||||
|
);
|
||||||
|
let mut report = Report::new(qmdl_path);
|
||||||
|
while let Some(container) = qmdl_stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.expect("failed getting QMDL container")
|
||||||
|
{
|
||||||
|
for row in harness.analyze_qmdl_messages(container) {
|
||||||
|
report.process_row(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.print_summary(show_skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pcapify(qmdl_path: &PathBuf) {
|
||||||
|
let qmdl_file = &mut File::open(&qmdl_path)
|
||||||
|
.await
|
||||||
|
.expect("failed to open qmdl file");
|
||||||
|
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
|
||||||
|
let mut pcap_path = qmdl_path.clone();
|
||||||
|
pcap_path.set_extension("pcapng");
|
||||||
|
let pcap_file = &mut File::create(&pcap_path)
|
||||||
|
.await
|
||||||
|
.expect("failed to open pcap file");
|
||||||
|
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
||||||
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
|
while let Some(container) = qmdl_reader
|
||||||
|
.get_next_messages_container()
|
||||||
|
.await
|
||||||
|
.expect("failed to get container")
|
||||||
|
{
|
||||||
|
for msg in container.into_messages().into_iter().flatten() {
|
||||||
|
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
|
||||||
|
pcap_writer
|
||||||
|
.write_gsmtap_message(parsed, timestamp)
|
||||||
|
.await
|
||||||
|
.expect("failed to write");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("wrote pcap to {:?}", &pcap_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let level = if args.debug {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else if args.quiet {
|
||||||
|
log::LevelFilter::Warn
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Info
|
||||||
|
};
|
||||||
|
simple_logger::SimpleLogger::new()
|
||||||
|
.with_colors(true)
|
||||||
|
.without_timestamps()
|
||||||
|
.with_level(level)
|
||||||
|
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
|
||||||
|
.with_module_level("asn1_codecs", log::LevelFilter::Error)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||||
|
info!("Analyzers:");
|
||||||
|
for analyzer in harness.get_metadata().analyzers {
|
||||||
|
info!(
|
||||||
|
" - {} (v{}): {}",
|
||||||
|
analyzer.name, analyzer.version, analyzer.description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for maybe_entry in WalkDir::new(&args.path) {
|
||||||
|
let Ok(entry) = maybe_entry else {
|
||||||
|
error!("failed to open dir entry {maybe_entry:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_str().unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
let path_str = path.to_str().unwrap();
|
||||||
|
// instead of relying on the QMDL extension, can we check if a file is
|
||||||
|
// QMDL by inspecting the contents?
|
||||||
|
if name_str.ends_with(".qmdl") {
|
||||||
|
info!("**** Beginning analysis of {name_str}");
|
||||||
|
analyze_qmdl(path_str, args.show_skipped).await;
|
||||||
|
if args.pcapify {
|
||||||
|
pcapify(&path.to_path_buf()).await;
|
||||||
|
}
|
||||||
|
} else if name_str.ends_with(".pcap") || name_str.ends_with(".pcapng") {
|
||||||
|
// TODO: if we've already analyzed a QMDL, skip its corresponding pcap
|
||||||
|
info!("**** Beginning analysis of {name_str}");
|
||||||
|
analyze_pcap(path_str, args.show_skipped).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
|
||||||
# These feature flags are mutually exclusive, and exactly one must be enabled.
|
|
||||||
orbic = ["rayhunter/orbic"]
|
|
||||||
tplink = ["rayhunter/tplink"]
|
|
||||||
wingtech = ["rayhunter/wingtech"]
|
|
||||||
|
|
||||||
default = ["orbic"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "rayhunter-daemon"
|
|
||||||
path = "src/daemon.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "rayhunter-check"
|
|
||||||
path = "src/check.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rayhunter = { path = "../lib" }
|
rayhunter = { path = "../lib" }
|
||||||
toml = "0.8.8"
|
toml = "0.8.8"
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] }
|
||||||
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
|
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
|
||||||
thiserror = "1.0.52"
|
thiserror = "1.0.52"
|
||||||
libc = "0.2.150"
|
libc = "0.2.150"
|
||||||
@@ -32,14 +16,12 @@ env_logger = { version = "0.11", default-features = false }
|
|||||||
tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
|
tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
|
||||||
futures-macro = "0.3.30"
|
futures-macro = "0.3.30"
|
||||||
include_dir = "0.7.3"
|
include_dir = "0.7.3"
|
||||||
mime_guess = "2.0.4"
|
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
tokio-stream = { version = "0.1.14", default-features = false }
|
tokio-stream = { version = "0.1.14", default-features = false }
|
||||||
futures = { version = "0.3.30", default-features = false }
|
futures = { version = "0.3.30", default-features = false }
|
||||||
clap = { version = "4.5.2", features = ["derive"] }
|
|
||||||
serde_json = "1.0.114"
|
serde_json = "1.0.114"
|
||||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
simple_logger = "5.0.0"
|
|
||||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -7,7 +7,7 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use log::{debug, error, info};
|
use log::{error, info};
|
||||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
|
use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
|
||||||
use rayhunter::diag::{DataType, MessagesContainer};
|
use rayhunter::diag::{DataType, MessagesContainer};
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlReader;
|
||||||
@@ -18,14 +18,12 @@ use tokio::sync::mpsc::Receiver;
|
|||||||
use tokio::sync::{RwLock, RwLockWriteGuard};
|
use tokio::sync::{RwLock, RwLockWriteGuard};
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use crate::dummy_analyzer::TestAnalyzer;
|
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
pub struct AnalysisWriter {
|
pub struct AnalysisWriter {
|
||||||
writer: BufWriter<File>,
|
writer: BufWriter<File>,
|
||||||
harness: Harness,
|
harness: Harness,
|
||||||
bytes_written: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We write our analysis results to a file immediately to minimize the amount of
|
// We write our analysis results to a file immediately to minimize the amount of
|
||||||
@@ -35,19 +33,11 @@ pub struct AnalysisWriter {
|
|||||||
// lets us simply append new rows to the end without parsing the entire JSON
|
// lets us simply append new rows to the end without parsing the entire JSON
|
||||||
// object beforehand.
|
// object beforehand.
|
||||||
impl AnalysisWriter {
|
impl AnalysisWriter {
|
||||||
pub async fn new(
|
pub async fn new(file: File, analyzer_config: &AnalyzerConfig) -> Result<Self, std::io::Error> {
|
||||||
file: File,
|
let harness = Harness::new_with_config(analyzer_config);
|
||||||
enable_dummy_analyzer: bool,
|
|
||||||
analyzer_config: &AnalyzerConfig,
|
|
||||||
) -> Result<Self, std::io::Error> {
|
|
||||||
let mut harness = Harness::new_with_config(analyzer_config);
|
|
||||||
if enable_dummy_analyzer {
|
|
||||||
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
writer: BufWriter::new(file),
|
writer: BufWriter::new(file),
|
||||||
bytes_written: 0,
|
|
||||||
harness,
|
harness,
|
||||||
};
|
};
|
||||||
let metadata = result.harness.get_metadata();
|
let metadata = result.harness.get_metadata();
|
||||||
@@ -56,22 +46,21 @@ impl AnalysisWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Runs the analysis harness on the given container, serializing the results
|
// Runs the analysis harness on the given container, serializing the results
|
||||||
// to the analysis file and returning the file's new length.
|
// to the analysis file, returning the whether any warnings were detected
|
||||||
pub async fn analyze(
|
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<bool, std::io::Error> {
|
||||||
&mut self,
|
let mut warning_detected = false;
|
||||||
container: MessagesContainer,
|
for row in self.harness.analyze_qmdl_messages(container) {
|
||||||
) -> Result<(usize, bool), std::io::Error> {
|
if !row.is_empty() {
|
||||||
let row = self.harness.analyze_qmdl_messages(container);
|
self.write(&row).await?;
|
||||||
if !row.is_empty() {
|
}
|
||||||
self.write(&row).await?;
|
warning_detected |= row.contains_warnings();
|
||||||
}
|
}
|
||||||
Ok((self.bytes_written, row.contains_warnings()))
|
Ok(warning_detected)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||||
let mut value_str = serde_json::to_string(value).unwrap();
|
let mut value_str = serde_json::to_string(value).unwrap();
|
||||||
value_str.push('\n');
|
value_str.push('\n');
|
||||||
self.bytes_written += value_str.len();
|
|
||||||
self.writer.write_all(value_str.as_bytes()).await?;
|
self.writer.write_all(value_str.as_bytes()).await?;
|
||||||
self.writer.flush().await?;
|
self.writer.flush().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -134,64 +123,58 @@ async fn finish_running_analysis(analysis_status_lock: Arc<RwLock<AnalysisStatus
|
|||||||
async fn perform_analysis(
|
async fn perform_analysis(
|
||||||
name: &str,
|
name: &str,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
enable_dummy_analyzer: bool,
|
|
||||||
analyzer_config: &AnalyzerConfig,
|
analyzer_config: &AnalyzerConfig,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
info!("Opening QMDL and analysis file for {}...", name);
|
info!("Opening QMDL and analysis file for {name}...");
|
||||||
let (analysis_file, qmdl_file, entry_index) = {
|
let (analysis_file, qmdl_file) = {
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
let (entry_index, _) = qmdl_store
|
let (entry_index, _) = qmdl_store
|
||||||
.entry_for_name(name)
|
.entry_for_name(name)
|
||||||
.ok_or(format!("failed to find QMDL store entry for {}", name))?;
|
.ok_or(format!("failed to find QMDL store entry for {name}"))?;
|
||||||
let analysis_file = qmdl_store
|
let analysis_file = qmdl_store
|
||||||
.clear_and_open_entry_analysis(entry_index)
|
.clear_and_open_entry_analysis(entry_index)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
let qmdl_file = qmdl_store
|
let qmdl_file = qmdl_store
|
||||||
.open_entry_qmdl(entry_index)
|
.open_entry_qmdl(entry_index)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
|
|
||||||
(analysis_file, qmdl_file, entry_index)
|
(analysis_file, qmdl_file)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut analysis_writer =
|
let mut analysis_writer = AnalysisWriter::new(analysis_file, analyzer_config)
|
||||||
AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config)
|
.await
|
||||||
.await
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
let file_size = qmdl_file
|
let file_size = qmdl_file
|
||||||
.metadata()
|
.metadata()
|
||||||
.await
|
.await
|
||||||
.expect("failed to get QMDL file metadata")
|
.expect("failed to get QMDL file metadata")
|
||||||
.len();
|
.len();
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
let mut qmdl_stream = pin::pin!(qmdl_reader
|
let mut qmdl_stream = pin::pin!(
|
||||||
.as_stream()
|
qmdl_reader
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
|
||||||
|
);
|
||||||
|
|
||||||
info!("Starting analysis for {}...", name);
|
info!("Starting analysis for {name}...");
|
||||||
while let Some(container) = qmdl_stream
|
while let Some(container) = qmdl_stream
|
||||||
.try_next()
|
.try_next()
|
||||||
.await
|
.await
|
||||||
.expect("failed getting QMDL container")
|
.expect("failed getting QMDL container")
|
||||||
{
|
{
|
||||||
let (size_bytes, _) = analysis_writer
|
let _ = analysis_writer
|
||||||
.analyze(container)
|
.analyze(container)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
debug!("{} analysis: {} bytes written", name, size_bytes);
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
qmdl_store
|
|
||||||
.update_entry_analysis_size(entry_index, size_bytes)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_writer
|
analysis_writer
|
||||||
.close()
|
.close()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{e:?}"))?;
|
||||||
info!("Analysis for {} complete!", name);
|
info!("Analysis for {name} complete!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -201,7 +184,6 @@ pub fn run_analysis_thread(
|
|||||||
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
|
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
enable_dummy_analyzer: bool,
|
|
||||||
analyzer_config: AnalyzerConfig,
|
analyzer_config: AnalyzerConfig,
|
||||||
) {
|
) {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
@@ -211,15 +193,10 @@ pub fn run_analysis_thread(
|
|||||||
let count = queued_len(analysis_status_lock.clone()).await;
|
let count = queued_len(analysis_status_lock.clone()).await;
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
||||||
if let Err(err) = perform_analysis(
|
if let Err(err) =
|
||||||
&name,
|
perform_analysis(&name, qmdl_store_lock.clone(), &analyzer_config).await
|
||||||
qmdl_store_lock.clone(),
|
|
||||||
enable_dummy_analyzer,
|
|
||||||
&analyzer_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
error!("failed to analyze {}: {}", name, err);
|
error!("failed to analyze {name}: {err}");
|
||||||
}
|
}
|
||||||
finish_running_analysis(analysis_status_lock.clone()).await;
|
finish_running_analysis(analysis_status_lock.clone()).await;
|
||||||
}
|
}
|
||||||
@@ -280,7 +257,7 @@ pub async fn start_analysis(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("failed to queue new analysis files: {:?}", e),
|
format!("failed to queue new analysis files: {e:?}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use rayhunter::Device;
|
||||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||||
|
|
||||||
use crate::error::RayhunterError;
|
use crate::error::RayhunterError;
|
||||||
@@ -10,8 +12,8 @@ pub struct Config {
|
|||||||
pub qmdl_store_path: String,
|
pub qmdl_store_path: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
pub device: Device,
|
||||||
pub ui_level: u8,
|
pub ui_level: u8,
|
||||||
pub enable_dummy_analyzer: bool,
|
|
||||||
pub colorblind_mode: bool,
|
pub colorblind_mode: bool,
|
||||||
pub key_input_mode: u8,
|
pub key_input_mode: u8,
|
||||||
pub analyzers: AnalyzerConfig,
|
pub analyzers: AnalyzerConfig,
|
||||||
@@ -23,8 +25,8 @@ impl Default for Config {
|
|||||||
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
device: Device::Orbic,
|
||||||
ui_level: 1,
|
ui_level: 1,
|
||||||
enable_dummy_analyzer: false,
|
|
||||||
colorblind_mode: false,
|
colorblind_mode: false,
|
||||||
key_input_mode: 0,
|
key_input_mode: 0,
|
||||||
analyzers: AnalyzerConfig::default(),
|
analyzers: AnalyzerConfig::default(),
|
||||||
@@ -39,6 +41,7 @@ where
|
|||||||
if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
|
if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
|
||||||
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
|
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
|
||||||
} else {
|
} else {
|
||||||
|
warn!("unable to read config file, using default config");
|
||||||
Ok(Config::default())
|
Ok(Config::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
use std::ops::DerefMut;
|
||||||
|
use std::pin::pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::http::header::CONTENT_TYPE;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use futures::{StreamExt, TryStreamExt};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||||
|
use rayhunter::diag::{DataType, MessagesContainer};
|
||||||
|
use rayhunter::diag_device::DiagDevice;
|
||||||
|
use rayhunter::qmdl::QmdlWriter;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tokio::sync::{RwLock, oneshot};
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||||
|
use crate::display;
|
||||||
|
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub enum DiagDeviceCtrlMessage {
|
||||||
|
StopRecording,
|
||||||
|
StartRecording,
|
||||||
|
DeleteEntry {
|
||||||
|
name: String,
|
||||||
|
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||||
|
},
|
||||||
|
DeleteAllEntries {
|
||||||
|
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||||
|
},
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiagTask {
|
||||||
|
ui_update_sender: Sender<display::DisplayState>,
|
||||||
|
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
analyzer_config: AnalyzerConfig,
|
||||||
|
state: DiagState,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DiagState {
|
||||||
|
Recording {
|
||||||
|
qmdl_writer: QmdlWriter<File>,
|
||||||
|
analysis_writer: Box<AnalysisWriter>,
|
||||||
|
},
|
||||||
|
Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagTask {
|
||||||
|
fn new(
|
||||||
|
ui_update_sender: Sender<display::DisplayState>,
|
||||||
|
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
analyzer_config: AnalyzerConfig,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
ui_update_sender,
|
||||||
|
analysis_sender,
|
||||||
|
analyzer_config,
|
||||||
|
state: DiagState::Stopped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start recording
|
||||||
|
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
||||||
|
let (qmdl_file, analysis_file) = qmdl_store
|
||||||
|
.new_entry()
|
||||||
|
.await
|
||||||
|
.expect("failed creating QMDL file entry");
|
||||||
|
self.stop_current_recording().await;
|
||||||
|
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||||
|
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
||||||
|
.await
|
||||||
|
.map(Box::new)
|
||||||
|
.expect("failed to write to analysis file");
|
||||||
|
self.state = DiagState::Recording {
|
||||||
|
qmdl_writer,
|
||||||
|
analysis_writer,
|
||||||
|
};
|
||||||
|
if let Err(e) = self
|
||||||
|
.ui_update_sender
|
||||||
|
.send(display::DisplayState::Recording)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("couldn't send ui update message: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop recording
|
||||||
|
async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
|
||||||
|
self.stop_current_recording().await;
|
||||||
|
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
||||||
|
if let Err(e) = self
|
||||||
|
.analysis_sender
|
||||||
|
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||||
|
entry.name.to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("couldn't send analysis message: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = qmdl_store.close_current_entry().await {
|
||||||
|
error!("couldn't close current entry: {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.ui_update_sender
|
||||||
|
.send(display::DisplayState::Paused)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("couldn't send ui update message: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_entry(
|
||||||
|
&mut self,
|
||||||
|
qmdl_store: &mut RecordingStore,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
|
if qmdl_store.is_current_entry(name) {
|
||||||
|
self.stop(qmdl_store).await;
|
||||||
|
}
|
||||||
|
let res = qmdl_store.delete_entry(name).await;
|
||||||
|
if let Err(e) = res.as_ref() {
|
||||||
|
error!("Error deleting QMDL entry {e}");
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_all_entries(
|
||||||
|
&mut self,
|
||||||
|
qmdl_store: &mut RecordingStore,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
|
self.stop(qmdl_store).await;
|
||||||
|
let res = qmdl_store.delete_all_entries().await;
|
||||||
|
if let Err(e) = res.as_ref() {
|
||||||
|
error!("Error deleting QMDL entries {e}");
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_current_recording(&mut self) {
|
||||||
|
let mut state = DiagState::Stopped;
|
||||||
|
std::mem::swap(&mut self.state, &mut state);
|
||||||
|
if let DiagState::Recording {
|
||||||
|
analysis_writer, ..
|
||||||
|
} = state
|
||||||
|
{
|
||||||
|
analysis_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("failed to close analysis writer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_container(
|
||||||
|
&mut self,
|
||||||
|
qmdl_store: &mut RecordingStore,
|
||||||
|
container: MessagesContainer,
|
||||||
|
) {
|
||||||
|
if container.data_type != DataType::UserSpace {
|
||||||
|
debug!("skipping non-userspace diag messages...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// keep track of how many bytes were written to the QMDL file so we can read
|
||||||
|
// a valid block of data from it in the HTTP server
|
||||||
|
if let DiagState::Recording {
|
||||||
|
qmdl_writer,
|
||||||
|
analysis_writer,
|
||||||
|
} = &mut self.state
|
||||||
|
{
|
||||||
|
qmdl_writer
|
||||||
|
.write_container(&container)
|
||||||
|
.await
|
||||||
|
.expect("failed to write to QMDL writer");
|
||||||
|
debug!(
|
||||||
|
"total QMDL bytes written: {}, updating manifest...",
|
||||||
|
qmdl_writer.total_written
|
||||||
|
);
|
||||||
|
let index = qmdl_store
|
||||||
|
.current_entry
|
||||||
|
.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||||
|
qmdl_store
|
||||||
|
.update_entry_qmdl_size(index, qmdl_writer.total_written)
|
||||||
|
.await
|
||||||
|
.expect("failed to update qmdl file size");
|
||||||
|
debug!("done!");
|
||||||
|
let heuristic_warning = analysis_writer
|
||||||
|
.analyze(container)
|
||||||
|
.await
|
||||||
|
.expect("failed to analyze container");
|
||||||
|
if heuristic_warning {
|
||||||
|
info!("a heuristic triggered on this run!");
|
||||||
|
self.ui_update_sender
|
||||||
|
.send(display::DisplayState::WarningDetected)
|
||||||
|
.await
|
||||||
|
.expect("couldn't send ui update message: {}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("no qmdl_writer set, continuing...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn run_diag_read_thread(
|
||||||
|
task_tracker: &TaskTracker,
|
||||||
|
mut dev: DiagDevice,
|
||||||
|
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||||
|
qmdl_file_tx: Sender<DiagDeviceCtrlMessage>,
|
||||||
|
ui_update_sender: Sender<display::DisplayState>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
analyzer_config: AnalyzerConfig,
|
||||||
|
) {
|
||||||
|
task_tracker.spawn(async move {
|
||||||
|
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||||
|
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config);
|
||||||
|
qmdl_file_tx
|
||||||
|
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
msg = qmdl_file_rx.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
diag_task.start(qmdl_store.deref_mut()).await;
|
||||||
|
},
|
||||||
|
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
diag_task.stop(qmdl_store.deref_mut()).await;
|
||||||
|
},
|
||||||
|
// None means all the Senders have been dropped, so it's
|
||||||
|
// time to go
|
||||||
|
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
||||||
|
info!("Diag reader thread exiting...");
|
||||||
|
diag_task.stop_current_recording().await;
|
||||||
|
return Ok(())
|
||||||
|
},
|
||||||
|
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
let resp = diag_task.delete_entry(qmdl_store.deref_mut(), name.as_str()).await;
|
||||||
|
if response_tx.send(resp).is_err() {
|
||||||
|
error!("Failed to send delete entry respons, receiver dropped");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx }) => {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
let resp = diag_task.delete_all_entries(qmdl_store.deref_mut()).await;
|
||||||
|
if response_tx.send(resp).is_err() {
|
||||||
|
error!("Failed to send delete all entries respons, receiver dropped");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maybe_container = diag_stream.next() => {
|
||||||
|
match maybe_container.unwrap() {
|
||||||
|
Ok(container) => {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
diag_task.process_container(qmdl_store.deref_mut(), container).await
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("error reading diag device: {err}");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start recording API for web thread
|
||||||
|
pub async fn start_recording(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
|
if state.config.debug_mode {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
.diag_device_ctrl_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send start recording message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop recording API for web thread
|
||||||
|
pub async fn stop_recording(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
|
if state.config.debug_mode {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.diag_device_ctrl_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send stop recording message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_recording(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
|
if state.config.debug_mode {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
|
}
|
||||||
|
let (response_tx, response_rx) = oneshot::channel();
|
||||||
|
state
|
||||||
|
.diag_device_ctrl_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::DeleteEntry {
|
||||||
|
name: qmdl_name.clone(),
|
||||||
|
response_tx,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send delete entry message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
match response_rx.await.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("failed to receive delete response: {e}"),
|
||||||
|
)
|
||||||
|
})? {
|
||||||
|
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||||
|
Err(RecordingStoreError::NoSuchEntryError) => Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("no recording with name {qmdl_name}"),
|
||||||
|
)),
|
||||||
|
Err(e) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't delete recording: {e}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_all_recordings(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
|
if state.config.debug_mode {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
|
}
|
||||||
|
let (response_tx, response_rx) = oneshot::channel();
|
||||||
|
state
|
||||||
|
.diag_device_ctrl_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx })
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send delete all entries message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
match response_rx.await.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("failed to receive delete all response: {e}"),
|
||||||
|
)
|
||||||
|
})? {
|
||||||
|
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||||
|
Err(e) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't delete recordings: {e}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_analysis_report(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
|
let (entry_index, _) = if qmdl_name == "live" {
|
||||||
|
qmdl_store.get_current_entry().ok_or((
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string(),
|
||||||
|
))?
|
||||||
|
} else {
|
||||||
|
qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("Couldn't find QMDL entry with name \"{qmdl_name}\""),
|
||||||
|
))?
|
||||||
|
};
|
||||||
|
let analysis_file = qmdl_store
|
||||||
|
.open_entry_analysis(entry_index)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||||
|
let analysis_stream = ReaderStream::new(analysis_file);
|
||||||
|
|
||||||
|
let headers = [(CONTENT_TYPE, "application/x-ndjson")];
|
||||||
|
let body = Body::from_stream(analysis_stream);
|
||||||
|
Ok((headers, body).into_response())
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage};
|
use async_trait::async_trait;
|
||||||
|
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -11,9 +12,7 @@ use tokio::sync::oneshot;
|
|||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use std::thread::sleep;
|
use include_dir::{Dir, include_dir};
|
||||||
|
|
||||||
use include_dir::{include_dir, Dir};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct Dimensions {
|
pub struct Dimensions {
|
||||||
@@ -65,15 +64,13 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait GenericFramebuffer: Send + 'static {
|
pub trait GenericFramebuffer: Send + 'static {
|
||||||
fn dimensions(&self) -> Dimensions;
|
fn dimensions(&self) -> Dimensions;
|
||||||
|
|
||||||
fn write_buffer(
|
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>); // rgb, row-wise, left-to-right, top-to-bottom
|
||||||
&mut self,
|
|
||||||
buffer: &[(u8, u8, u8)], // rgb, row-wise, left-to-right, top-to-bottom
|
|
||||||
);
|
|
||||||
|
|
||||||
fn write_dynamic_image(&mut self, img: DynamicImage) {
|
async fn write_dynamic_image(&mut self, img: DynamicImage) {
|
||||||
let dimensions = self.dimensions();
|
let dimensions = self.dimensions();
|
||||||
let mut width = img.width();
|
let mut width = img.width();
|
||||||
let mut height = img.height();
|
let mut height = img.height();
|
||||||
@@ -94,28 +91,35 @@ pub trait GenericFramebuffer: Send + 'static {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_buffer(&buf);
|
self.write_buffer(buf).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_gif(&mut self, img_buffer: &[u8]) {
|
async fn draw_gif(&mut self, img_buffer: &[u8]) {
|
||||||
// this is dumb and i'm sure there's a better way to loop this
|
|
||||||
let cursor = Cursor::new(img_buffer);
|
let cursor = Cursor::new(img_buffer);
|
||||||
let decoder = GifDecoder::new(cursor).unwrap();
|
if let Ok(decoder) = GifDecoder::new(cursor) {
|
||||||
for maybe_frame in decoder.into_frames() {
|
let frames: Vec<_> = decoder
|
||||||
let frame = maybe_frame.unwrap();
|
.into_frames()
|
||||||
let (numerator, _) = frame.delay().numer_denom_ms();
|
.filter_map(|f| f.ok())
|
||||||
let img = DynamicImage::from(frame.into_buffer());
|
.map(|frame| {
|
||||||
self.write_dynamic_image(img);
|
let (numerator, _) = frame.delay().numer_denom_ms();
|
||||||
std::thread::sleep(Duration::from_millis(numerator as u64));
|
let img = DynamicImage::from(frame.into_buffer());
|
||||||
|
(img, numerator as u64)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (img, delay_ms) in frames {
|
||||||
|
self.write_dynamic_image(img).await;
|
||||||
|
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_img(&mut self, img_buffer: &[u8]) {
|
async fn draw_img(&mut self, img_buffer: &[u8]) {
|
||||||
let img = image::load_from_memory(img_buffer).unwrap();
|
let img = image::load_from_memory(img_buffer).unwrap();
|
||||||
self.write_dynamic_image(img);
|
self.write_dynamic_image(img).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_line(&mut self, color: Color, height: u32) {
|
async fn draw_line(&mut self, color: Color, height: u32) {
|
||||||
let width = self.dimensions().width;
|
let width = self.dimensions().width;
|
||||||
let px_num = height * width;
|
let px_num = height * width;
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
@@ -123,7 +127,7 @@ pub trait GenericFramebuffer: Send + 'static {
|
|||||||
buffer.push(color.rgb());
|
buffer.push(color.rgb());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_buffer(&buffer);
|
self.write_buffer(buffer).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +147,7 @@ pub fn update_ui(
|
|||||||
let colorblind_mode = config.colorblind_mode;
|
let colorblind_mode = config.colorblind_mode;
|
||||||
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
|
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
|
||||||
|
|
||||||
task_tracker.spawn_blocking(move || {
|
task_tracker.spawn(async move {
|
||||||
// this feels wrong, is there a more rusty way to do this?
|
// this feels wrong, is there a more rusty way to do this?
|
||||||
let mut img: Option<&[u8]> = None;
|
let mut img: Option<&[u8]> = None;
|
||||||
if display_level == 2 {
|
if display_level == 2 {
|
||||||
@@ -179,24 +183,21 @@ pub fn update_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match display_level {
|
match display_level {
|
||||||
2 => {
|
2 => fb.draw_gif(img.unwrap()).await,
|
||||||
fb.draw_gif(img.unwrap());
|
3 => fb.draw_img(img.unwrap()).await,
|
||||||
}
|
|
||||||
3 => fb.draw_img(img.unwrap()),
|
|
||||||
128 => {
|
128 => {
|
||||||
fb.draw_line(Color::Cyan, 128);
|
fb.draw_line(Color::Cyan, 128).await;
|
||||||
fb.draw_line(Color::Pink, 102);
|
fb.draw_line(Color::Pink, 102).await;
|
||||||
fb.draw_line(Color::White, 76);
|
fb.draw_line(Color::White, 76).await;
|
||||||
fb.draw_line(Color::Pink, 50);
|
fb.draw_line(Color::Pink, 50).await;
|
||||||
fb.draw_line(Color::Cyan, 25);
|
fb.draw_line(Color::Cyan, 25).await;
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// this branch id for ui_level 1, which is also the default if an
|
|
||||||
// unknown value is used
|
|
||||||
fb.draw_line(display_color, 2);
|
|
||||||
}
|
}
|
||||||
|
// this branch id for ui_level 1, which is also the default if an
|
||||||
|
// unknown value is used
|
||||||
|
_ => {}
|
||||||
};
|
};
|
||||||
sleep(Duration::from_millis(1000));
|
fb.draw_line(display_color, 2).await;
|
||||||
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use log::info;
|
||||||
|
use tokio::sync::mpsc::Receiver;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::display::DisplayState;
|
||||||
|
|
||||||
|
pub fn update_ui(
|
||||||
|
_task_tracker: &TaskTracker,
|
||||||
|
_config: &config::Config,
|
||||||
|
_ui_shutdown_rx: oneshot::Receiver<()>,
|
||||||
|
_ui_update_rx: Receiver<DisplayState>,
|
||||||
|
) {
|
||||||
|
info!("Headless mode, not spawning UI.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
mod generic_framebuffer;
|
||||||
|
|
||||||
|
pub mod headless;
|
||||||
|
pub mod orbic;
|
||||||
|
pub mod tmobile;
|
||||||
|
pub mod tplink;
|
||||||
|
pub mod tplink_framebuffer;
|
||||||
|
pub mod tplink_onebit;
|
||||||
|
pub mod wingtech;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum DisplayState {
|
||||||
|
Recording,
|
||||||
|
Paused,
|
||||||
|
WarningDetected,
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
|
||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
|
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
@@ -11,6 +12,7 @@ const FB_PATH: &str = "/dev/fb0";
|
|||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
struct Framebuffer;
|
struct Framebuffer;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl GenericFramebuffer for Framebuffer {
|
impl GenericFramebuffer for Framebuffer {
|
||||||
fn dimensions(&self) -> Dimensions {
|
fn dimensions(&self) -> Dimensions {
|
||||||
// TODO actually poll for this, maybe w/ fbset?
|
// TODO actually poll for this, maybe w/ fbset?
|
||||||
@@ -20,16 +22,16 @@ impl GenericFramebuffer for Framebuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||||
let mut raw_buffer = Vec::new();
|
let mut raw_buffer = Vec::new();
|
||||||
for (r, g, b) in buffer {
|
for (r, g, b) in buffer {
|
||||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||||
rgb565 |= (*b as u16) >> 3;
|
rgb565 |= (b as u16) >> 3;
|
||||||
raw_buffer.extend(rgb565.to_le_bytes());
|
raw_buffer.extend(rgb565.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/// Display module for Tmobile TMOHS1, blink LEDs on the front of the device.
|
||||||
|
/// DisplayState::Recording => Signal LED slowly blinks blue.
|
||||||
|
/// DisplayState::Paused => WiFi LED blinks white.
|
||||||
|
/// DisplayState::WarningDetected => Signal LED slowly blinks red.
|
||||||
|
use log::{error, info};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::display::DisplayState;
|
||||||
|
|
||||||
|
macro_rules! led {
|
||||||
|
($l:expr) => {{ format!("/sys/class/leds/led:{}/blink", $l) }};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_blinking(path: String) {
|
||||||
|
tokio::fs::write(&path, "1").await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_blinking(path: String) {
|
||||||
|
tokio::fs::write(&path, "0").await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_ui(
|
||||||
|
task_tracker: &TaskTracker,
|
||||||
|
config: &config::Config,
|
||||||
|
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||||
|
mut ui_update_rx: mpsc::Receiver<DisplayState>,
|
||||||
|
) {
|
||||||
|
let mut invisible: bool = false;
|
||||||
|
if config.ui_level == 0 {
|
||||||
|
info!("Invisible mode, not spawning UI.");
|
||||||
|
invisible = true;
|
||||||
|
}
|
||||||
|
task_tracker.spawn(async move {
|
||||||
|
let mut state = DisplayState::Recording;
|
||||||
|
let mut last_state = DisplayState::Paused;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match ui_shutdown_rx.try_recv() {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("received UI shutdown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(oneshot::error::TryRecvError::Empty) => {}
|
||||||
|
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||||
|
}
|
||||||
|
match ui_update_rx.try_recv() {
|
||||||
|
Ok(new_state) => state = new_state,
|
||||||
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||||
|
Err(e) => error!("error receiving ui update message: {e}"),
|
||||||
|
};
|
||||||
|
if invisible || state == last_state {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match state {
|
||||||
|
DisplayState::Paused => {
|
||||||
|
stop_blinking(led!("signal_blue")).await;
|
||||||
|
stop_blinking(led!("signal_red")).await;
|
||||||
|
start_blinking(led!("wlan_white")).await;
|
||||||
|
}
|
||||||
|
DisplayState::Recording => {
|
||||||
|
stop_blinking(led!("wlan_white")).await;
|
||||||
|
stop_blinking(led!("signal_red")).await;
|
||||||
|
start_blinking(led!("signal_blue")).await;
|
||||||
|
}
|
||||||
|
DisplayState::WarningDetected => {
|
||||||
|
stop_blinking(led!("wlan_white")).await;
|
||||||
|
stop_blinking(led!("signal_blue")).await;
|
||||||
|
start_blinking(led!("signal_red")).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_state = state;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use tokio::sync::oneshot;
|
|||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::{tplink_framebuffer, tplink_onebit, DisplayState};
|
use crate::display::{DisplayState, tplink_framebuffer, tplink_onebit};
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ pub fn update_ui(
|
|||||||
info!("Invisible mode, not spawning UI.");
|
info!("Invisible mode, not spawning UI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since this is a one-time check at startup, using sync is acceptable
|
||||||
|
// The alternative would be to make the entire initialization async
|
||||||
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
|
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
|
||||||
info!("detected one-bit display");
|
info!("detected one-bit display");
|
||||||
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
|
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::fs::File;
|
use async_trait::async_trait;
|
||||||
use std::io::Write;
|
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
|
use tokio::fs::OpenOptions;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
|
||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
|
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
@@ -24,6 +25,7 @@ struct fb_fillrect {
|
|||||||
rop: u32,
|
rop: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl GenericFramebuffer for Framebuffer {
|
impl GenericFramebuffer for Framebuffer {
|
||||||
fn dimensions(&self) -> Dimensions {
|
fn dimensions(&self) -> Dimensions {
|
||||||
// TODO actually poll for this, maybe w/ fbset?
|
// TODO actually poll for this, maybe w/ fbset?
|
||||||
@@ -33,12 +35,12 @@ impl GenericFramebuffer for Framebuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||||
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
|
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
|
||||||
let dimensions = self.dimensions();
|
let dimensions = self.dimensions();
|
||||||
let width = dimensions.width;
|
let width = dimensions.width;
|
||||||
let height = buffer.len() as u32 / width;
|
let height = buffer.len() as u32 / width;
|
||||||
let mut f = File::options().write(true).open(FB_PATH).unwrap();
|
let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
|
||||||
let mut arg = fb_fillrect {
|
let mut arg = fb_fillrect {
|
||||||
dx: 0,
|
dx: 0,
|
||||||
dy: 0,
|
dy: 0,
|
||||||
@@ -50,15 +52,16 @@ impl GenericFramebuffer for Framebuffer {
|
|||||||
|
|
||||||
let mut raw_buffer = Vec::new();
|
let mut raw_buffer = Vec::new();
|
||||||
for (r, g, b) in buffer {
|
for (r, g, b) in buffer {
|
||||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||||
rgb565 |= (*b as u16) >> 3;
|
rgb565 |= (b as u16) >> 3;
|
||||||
// note: big-endian!
|
// note: big-endian!
|
||||||
raw_buffer.extend(rgb565.to_be_bytes());
|
raw_buffer.extend(rgb565.to_be_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_all(&raw_buffer).unwrap();
|
f.write_all(&raw_buffer).await.unwrap();
|
||||||
|
|
||||||
|
// ioctl is a synchronous operation, but it's fast enough that it shouldn't block
|
||||||
unsafe {
|
unsafe {
|
||||||
let res = libc::ioctl(
|
let res = libc::ioctl(
|
||||||
f.as_raw_fd(),
|
f.as_raw_fd(),
|
||||||
@@ -68,7 +71,7 @@ impl GenericFramebuffer for Framebuffer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if res < 0 {
|
if res < 0 {
|
||||||
panic!("failed to send FBIORECT_DISPLAY ioctl, {}", res);
|
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,6 @@ use tokio::sync::oneshot;
|
|||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::thread::sleep;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer";
|
pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer";
|
||||||
@@ -122,7 +120,7 @@ pub fn update_ui(
|
|||||||
info!("Invisible mode, not spawning UI.");
|
info!("Invisible mode, not spawning UI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
task_tracker.spawn_blocking(move || {
|
task_tracker.spawn(async move {
|
||||||
let mut pixels = STATUS_SMILING;
|
let mut pixels = STATUS_SMILING;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -148,12 +146,12 @@ pub fn update_ui(
|
|||||||
// we write the status every second because it may have been overwritten through menu
|
// we write the status every second because it may have been overwritten through menu
|
||||||
// navigation.
|
// navigation.
|
||||||
if display_level != 0 {
|
if display_level != 0 {
|
||||||
if let Err(e) = fs::write(OLED_PATH, &pixels) {
|
if let Err(e) = tokio::fs::write(OLED_PATH, pixels).await {
|
||||||
error!("failed to write to display: {e}");
|
error!("failed to write to display: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(Duration::from_millis(1000));
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
use crate::config;
|
||||||
|
use crate::display::DisplayState;
|
||||||
|
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||||
///
|
///
|
||||||
/// Tested on (from `/etc/wt_version`):
|
/// Tested on (from `/etc/wt_version`):
|
||||||
/// 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 crate::config;
|
use async_trait::async_trait;
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
|
||||||
use crate::display::DisplayState;
|
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
@@ -17,6 +18,7 @@ const FB_PATH: &str = "/dev/fb0";
|
|||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
struct Framebuffer;
|
struct Framebuffer;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl GenericFramebuffer for Framebuffer {
|
impl GenericFramebuffer for Framebuffer {
|
||||||
fn dimensions(&self) -> Dimensions {
|
fn dimensions(&self) -> Dimensions {
|
||||||
Dimensions {
|
Dimensions {
|
||||||
@@ -25,16 +27,16 @@ impl GenericFramebuffer for Framebuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||||
let mut raw_buffer = Vec::new();
|
let mut raw_buffer = Vec::new();
|
||||||
for (r, g, b) in buffer {
|
for (r, g, b) in buffer {
|
||||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||||
rgb565 |= (*b as u16) >> 3;
|
rgb565 |= (b as u16) >> 3;
|
||||||
raw_buffer.extend(rgb565.to_le_bytes());
|
raw_buffer.extend(rgb565.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ pub fn run_key_input_thread(
|
|||||||
let mut file = match File::open("/dev/input/event0").await {
|
let mut file = match File::open("/dev/input/event0").await {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to open /dev/input/event0: {}", e);
|
error!("Failed to open /dev/input/event0: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -49,7 +49,7 @@ pub fn run_key_input_thread(
|
|||||||
}
|
}
|
||||||
result = file.read_exact(&mut buffer) => {
|
result = file.read_exact(&mut buffer) => {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
error!("failed to read key input: {}", e);
|
error!("failed to read key input: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,12 +79,12 @@ pub fn run_key_input_thread(
|
|||||||
{
|
{
|
||||||
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StopRecording).await
|
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StopRecording).await
|
||||||
{
|
{
|
||||||
error!("Failed to send StopRecording: {}", e);
|
error!("Failed to send StopRecording: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
|
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
|
||||||
{
|
{
|
||||||
error!("Failed to send StartRecording: {}", e);
|
error!("Failed to send StartRecording: {e}");
|
||||||
}
|
}
|
||||||
last_keyup = None;
|
last_keyup = None;
|
||||||
continue;
|
continue;
|
||||||
@@ -2,7 +2,6 @@ mod analysis;
|
|||||||
mod config;
|
mod config;
|
||||||
mod diag;
|
mod diag;
|
||||||
mod display;
|
mod display;
|
||||||
mod dummy_analyzer;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod key_input;
|
mod key_input;
|
||||||
mod pcap;
|
mod pcap;
|
||||||
@@ -11,34 +10,35 @@ mod server;
|
|||||||
mod stats;
|
mod stats;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
use crate::config::{parse_args, parse_config};
|
use crate::config::{parse_args, parse_config};
|
||||||
use crate::diag::run_diag_read_thread;
|
use crate::diag::run_diag_read_thread;
|
||||||
use crate::error::RayhunterError;
|
use crate::error::RayhunterError;
|
||||||
use crate::pcap::get_pcap;
|
use crate::pcap::get_pcap;
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState};
|
use crate::server::{ServerState, get_config, get_qmdl, get_zip, serve_static, set_config};
|
||||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||||
|
|
||||||
use analysis::{
|
use analysis::{
|
||||||
get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus,
|
AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis,
|
||||||
};
|
};
|
||||||
|
use axum::Router;
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::Router;
|
|
||||||
use diag::{
|
use diag::{
|
||||||
delete_all_recordings, delete_recording, get_analysis_report, start_recording, stop_recording,
|
DiagDeviceCtrlMessage, delete_all_recordings, delete_recording, get_analysis_report,
|
||||||
DiagDeviceCtrlMessage,
|
start_recording, stop_recording,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use qmdl_store::RecordingStoreError;
|
use qmdl_store::RecordingStoreError;
|
||||||
|
use rayhunter::Device;
|
||||||
use rayhunter::diag_device::DiagDevice;
|
use rayhunter::diag_device::DiagDevice;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::mpsc::{self, Sender};
|
use tokio::sync::mpsc::{self, Sender};
|
||||||
use tokio::sync::{oneshot, RwLock};
|
use tokio::sync::{RwLock, oneshot};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
|
|||||||
|
|
||||||
// Loads a RecordingStore if one exists, and if not, only create one if we're
|
// Loads a RecordingStore if one exists, and if not, only create one if we're
|
||||||
// not in debug mode. If we fail to parse the manifest AND we're not in debug
|
// not in debug mode. If we fail to parse the manifest AND we're not in debug
|
||||||
// mode, try to recover by making a new (empty) manifest in the same directory.
|
// mode, try to recover the manifest from the existing QMDL files
|
||||||
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
|
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
|
||||||
let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
|
let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
|
||||||
if config.debug_mode {
|
if config.debug_mode {
|
||||||
@@ -108,9 +108,9 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
|
|||||||
match RecordingStore::load(&config.qmdl_store_path).await {
|
match RecordingStore::load(&config.qmdl_store_path).await {
|
||||||
Ok(store) => Ok(store),
|
Ok(store) => Ok(store),
|
||||||
Err(RecordingStoreError::ParseManifestError(err)) => {
|
Err(RecordingStoreError::ParseManifestError(err)) => {
|
||||||
error!("failed to parse QMDL manifest: {}", err);
|
error!("failed to parse QMDL manifest: {err}");
|
||||||
info!("creating new empty manifest...");
|
info!("recovering manifest from existing QMDL files...");
|
||||||
Ok(RecordingStore::create(&config.qmdl_store_path).await?)
|
Ok(RecordingStore::recover(&config.qmdl_store_path).await?)
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
@@ -122,6 +122,7 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
|
|||||||
// Start a thread that'll track when user hits ctrl+c. When that happens,
|
// Start a thread that'll track when user hits ctrl+c. When that happens,
|
||||||
// trigger various cleanup tasks, including sending signals to other threads to
|
// trigger various cleanup tasks, including sending signals to other threads to
|
||||||
// shutdown
|
// shutdown
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run_shutdown_thread(
|
fn run_shutdown_thread(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
@@ -139,14 +140,14 @@ fn run_shutdown_thread(
|
|||||||
select! {
|
select! {
|
||||||
res = tokio::signal::ctrl_c() => {
|
res = tokio::signal::ctrl_c() => {
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("Unable to listen for shutdown signal: {}", err);
|
error!("Unable to listen for shutdown signal: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
should_restart_flag.store(false, Ordering::Relaxed);
|
should_restart_flag.store(false, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
res = daemon_restart_rx => {
|
res = daemon_restart_rx => {
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("Unable to listen for shutdown signal: {}", err);
|
error!("Unable to listen for shutdown signal: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
should_restart_flag.store(true, Ordering::Relaxed);
|
should_restart_flag.store(true, Ordering::Relaxed);
|
||||||
@@ -181,7 +182,7 @@ fn run_shutdown_thread(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<(), RayhunterError> {
|
async fn main() -> Result<(), RayhunterError> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
@@ -215,7 +216,8 @@ async fn run_with_config(
|
|||||||
if !config.debug_mode {
|
if !config.debug_mode {
|
||||||
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
||||||
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
|
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
|
||||||
let mut dev = DiagDevice::new()
|
info!("Using configuration for device: {0:?}", config.device);
|
||||||
|
let mut dev = DiagDevice::new(&config.device)
|
||||||
.await
|
.await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
dev.config_logs()
|
dev.config_logs()
|
||||||
@@ -227,14 +229,22 @@ async fn run_with_config(
|
|||||||
&task_tracker,
|
&task_tracker,
|
||||||
dev,
|
dev,
|
||||||
diag_rx,
|
diag_rx,
|
||||||
|
diag_tx.clone(),
|
||||||
ui_update_tx.clone(),
|
ui_update_tx.clone(),
|
||||||
qmdl_store_lock.clone(),
|
qmdl_store_lock.clone(),
|
||||||
analysis_tx.clone(),
|
analysis_tx.clone(),
|
||||||
config.enable_dummy_analyzer,
|
|
||||||
config.analyzers.clone(),
|
config.analyzers.clone(),
|
||||||
);
|
);
|
||||||
info!("Starting UI");
|
info!("Starting UI");
|
||||||
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
|
||||||
|
let update_ui = match &config.device {
|
||||||
|
Device::Orbic => display::orbic::update_ui,
|
||||||
|
Device::Tplink => display::tplink::update_ui,
|
||||||
|
Device::Tmobile => display::tmobile::update_ui,
|
||||||
|
Device::Wingtech => display::wingtech::update_ui,
|
||||||
|
Device::Pinephone => display::headless::update_ui,
|
||||||
|
};
|
||||||
|
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||||
|
|
||||||
info!("Starting Key Input service");
|
info!("Starting Key Input service");
|
||||||
let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel();
|
let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel();
|
||||||
@@ -255,7 +265,6 @@ async fn run_with_config(
|
|||||||
analysis_rx,
|
analysis_rx,
|
||||||
qmdl_store_lock.clone(),
|
qmdl_store_lock.clone(),
|
||||||
analysis_status_lock.clone(),
|
analysis_status_lock.clone(),
|
||||||
config.enable_dummy_analyzer,
|
|
||||||
config.analyzers.clone(),
|
config.analyzers.clone(),
|
||||||
);
|
);
|
||||||
let should_restart_flag = Arc::new(AtomicBool::new(false));
|
let should_restart_flag = Arc::new(AtomicBool::new(false));
|
||||||
@@ -276,7 +285,6 @@ async fn run_with_config(
|
|||||||
config,
|
config,
|
||||||
qmdl_store_lock: qmdl_store_lock.clone(),
|
qmdl_store_lock: qmdl_store_lock.clone(),
|
||||||
diag_device_ctrl_sender: diag_tx,
|
diag_device_ctrl_sender: diag_tx,
|
||||||
ui_update_sender: ui_update_tx,
|
|
||||||
analysis_status_lock,
|
analysis_status_lock,
|
||||||
analysis_sender: analysis_tx,
|
analysis_sender: analysis_tx,
|
||||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||||
@@ -3,8 +3,8 @@ use crate::ServerState;
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::header::CONTENT_TYPE;
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use log::error;
|
use log::error;
|
||||||
use rayhunter::diag::DataType;
|
use rayhunter::diag::DataType;
|
||||||
@@ -12,7 +12,7 @@ use rayhunter::gsmtap_parser;
|
|||||||
use rayhunter::pcap::GsmtapPcapWriter;
|
use rayhunter::pcap::GsmtapPcapWriter;
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlReader;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{duplex, AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite, duplex};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
||||||
@@ -28,7 +28,7 @@ pub async fn get_pcap(
|
|||||||
}
|
}
|
||||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
format!("couldn't find manifest entry with name {}", qmdl_name),
|
format!("couldn't find manifest entry with name {qmdl_name}"),
|
||||||
))?;
|
))?;
|
||||||
if entry.qmdl_size_bytes == 0 {
|
if entry.qmdl_size_bytes == 0 {
|
||||||
return Err((
|
return Err((
|
||||||
@@ -40,14 +40,14 @@ pub async fn get_pcap(
|
|||||||
let qmdl_file = qmdl_store
|
let qmdl_file = qmdl_store
|
||||||
.open_entry_qmdl(entry_index)
|
.open_entry_qmdl(entry_index)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||||
// the QMDL reader should stop at the last successfully written data chunk
|
// the QMDL reader should stop at the last successfully written data chunk
|
||||||
// (entry.size_bytes)
|
// (entry.size_bytes)
|
||||||
let (reader, writer) = duplex(1024);
|
let (reader, writer) = duplex(1024);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes).await {
|
if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes).await {
|
||||||
error!("failed to generate PCAP: {:?}", e);
|
error!("failed to generate PCAP: {e:?}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("error parsing message: {:?}", e),
|
Err(e) => error!("error parsing message: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::io::{self, ErrorKind};
|
use std::io::{self, ErrorKind};
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
|
use log::{info, warn};
|
||||||
use rayhunter::util::RuntimeMetadata;
|
use rayhunter::util::RuntimeMetadata;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{self, try_exists, File, OpenOptions},
|
fs::{self, File, OpenOptions, try_exists},
|
||||||
io::AsyncWriteExt,
|
io::AsyncWriteExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +51,6 @@ pub struct ManifestEntry {
|
|||||||
pub start_time: DateTime<Local>,
|
pub start_time: DateTime<Local>,
|
||||||
pub last_message_time: Option<DateTime<Local>>,
|
pub last_message_time: Option<DateTime<Local>>,
|
||||||
pub qmdl_size_bytes: usize,
|
pub qmdl_size_bytes: usize,
|
||||||
pub analysis_size_bytes: usize,
|
|
||||||
pub rayhunter_version: Option<String>,
|
pub rayhunter_version: Option<String>,
|
||||||
pub system_os: Option<String>,
|
pub system_os: Option<String>,
|
||||||
pub arch: Option<String>,
|
pub arch: Option<String>,
|
||||||
@@ -64,7 +65,6 @@ impl ManifestEntry {
|
|||||||
start_time: now,
|
start_time: now,
|
||||||
last_message_time: None,
|
last_message_time: None,
|
||||||
qmdl_size_bytes: 0,
|
qmdl_size_bytes: 0,
|
||||||
analysis_size_bytes: 0,
|
|
||||||
rayhunter_version: Some(metadata.rayhunter_version),
|
rayhunter_version: Some(metadata.rayhunter_version),
|
||||||
system_os: Some(metadata.system_os),
|
system_os: Some(metadata.system_os),
|
||||||
arch: Some(metadata.arch),
|
arch: Some(metadata.arch),
|
||||||
@@ -138,6 +138,83 @@ impl RecordingStore {
|
|||||||
Ok(store)
|
Ok(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does a best-effort attempt to recover the manifest from a directory of
|
||||||
|
// QMDL files. We expect these files to be named like "<timestamp>.qmdl",
|
||||||
|
// and skip any files which don't match that pattern.
|
||||||
|
pub async fn recover<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut dir_entries = fs::read_dir(path.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::OpenDirError)?;
|
||||||
|
let mut manifest_entries = Vec::new();
|
||||||
|
|
||||||
|
while let Some(entry) = dir_entries
|
||||||
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::OpenDirError)?
|
||||||
|
{
|
||||||
|
let os_filename = entry.file_name();
|
||||||
|
let Some(filename) = os_filename.to_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !filename.ends_with(".qmdl") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stem = filename.trim_end_matches(".qmdl");
|
||||||
|
let Ok(start_timestamp) = stem.parse::<i64>() else {
|
||||||
|
warn!("QMDL file has invalid name {os_filename:?}, skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = match entry.metadata().await {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("failed to read QMDL file metadata: {err:?}, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(start_time) = DateTime::from_timestamp(start_timestamp, 0) else {
|
||||||
|
warn!("QMDL filename {os_filename:?} gave an invalid timestamp, skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(last_message_time) = metadata.modified() else {
|
||||||
|
warn!("failed to get modified time for QMDL file {os_filename:?}, skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("successfully recovered QMDL entry {os_filename:?}!");
|
||||||
|
manifest_entries.push(ManifestEntry {
|
||||||
|
name: stem.to_string(),
|
||||||
|
start_time: start_time.into(),
|
||||||
|
last_message_time: Some(last_message_time.into()),
|
||||||
|
qmdl_size_bytes: metadata.size() as usize,
|
||||||
|
rayhunter_version: None,
|
||||||
|
system_os: None,
|
||||||
|
arch: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort chronologically
|
||||||
|
manifest_entries.sort_by(|a, b| a.start_time.cmp(&b.start_time));
|
||||||
|
|
||||||
|
let mut store = RecordingStore {
|
||||||
|
path: path.as_ref().to_path_buf(),
|
||||||
|
manifest: Manifest {
|
||||||
|
entries: manifest_entries,
|
||||||
|
},
|
||||||
|
current_entry: None,
|
||||||
|
};
|
||||||
|
store.write_manifest().await?;
|
||||||
|
|
||||||
|
Ok(store)
|
||||||
|
}
|
||||||
|
|
||||||
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
|
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
@@ -202,7 +279,6 @@ impl RecordingStore {
|
|||||||
.open(entry.get_analysis_filepath(&self.path))
|
.open(entry.get_analysis_filepath(&self.path))
|
||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::ReadFileError)?;
|
.map_err(RecordingStoreError::ReadFileError)?;
|
||||||
self.update_entry_analysis_size(entry_index, 0).await?;
|
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,17 +304,9 @@ impl RecordingStore {
|
|||||||
self.write_manifest().await
|
self.write_manifest().await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the given entry's analysis file size
|
|
||||||
pub async fn update_entry_analysis_size(
|
|
||||||
&mut self,
|
|
||||||
entry_index: usize,
|
|
||||||
size_bytes: usize,
|
|
||||||
) -> Result<(), RecordingStoreError> {
|
|
||||||
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
|
|
||||||
self.write_manifest().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
|
// we don't technically need a mutable reference to `self` here, but it
|
||||||
|
// does prevent multiple concurrent writes across different threads
|
||||||
let tmp_path = self.path.join("manifest.toml.new");
|
let tmp_path = self.path.join("manifest.toml.new");
|
||||||
let mut manifest_tmp_file = File::create(&tmp_path)
|
let mut manifest_tmp_file = File::create(&tmp_path)
|
||||||
.await
|
.await
|
||||||
@@ -273,20 +341,32 @@ impl RecordingStore {
|
|||||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_entry(&mut self, name: &str) -> Result<ManifestEntry, RecordingStoreError> {
|
pub fn is_current_entry(&self, name: &str) -> bool {
|
||||||
|
match self.current_entry {
|
||||||
|
Some(idx) => match self.manifest.entries.get(idx) {
|
||||||
|
Some(entry) => entry.name == name,
|
||||||
|
None => false,
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_entry(&mut self, name: &str) -> Result<(), RecordingStoreError> {
|
||||||
let entry_to_delete_idx = self
|
let entry_to_delete_idx = self
|
||||||
.manifest
|
.manifest
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|entry| entry.name == name)
|
.position(|entry| entry.name == name)
|
||||||
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
||||||
if let Some(current_entry) = self.current_entry {
|
match self.current_entry {
|
||||||
if current_entry == entry_to_delete_idx {
|
Some(current_entry) if current_entry == entry_to_delete_idx => {
|
||||||
self.close_current_entry().await?;
|
self.close_current_entry().await?;
|
||||||
} else {
|
}
|
||||||
|
Some(current_entry) => {
|
||||||
self.current_entry = Some(current_entry - 1);
|
self.current_entry = Some(current_entry - 1);
|
||||||
}
|
}
|
||||||
}
|
None => {}
|
||||||
|
};
|
||||||
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
|
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
|
||||||
self.write_manifest().await?;
|
self.write_manifest().await?;
|
||||||
let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path);
|
let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path);
|
||||||
@@ -297,7 +377,7 @@ impl RecordingStore {
|
|||||||
remove_file_if_exists(&analysis_filepath)
|
remove_file_if_exists(&analysis_filepath)
|
||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||||
Ok(entry_to_delete)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
|
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
@@ -369,9 +449,11 @@ mod tests {
|
|||||||
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||||
store.manifest
|
store.manifest
|
||||||
);
|
);
|
||||||
assert!(store.manifest.entries[entry_index]
|
assert!(
|
||||||
.last_message_time
|
store.manifest.entries[entry_index]
|
||||||
.is_none());
|
.last_message_time
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
store
|
store
|
||||||
.update_entry_qmdl_size(entry_index, 1000)
|
.update_entry_qmdl_size(entry_index, 1000)
|
||||||
@@ -1,36 +1,34 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use async_zip::tokio::write::ZipFileWriter;
|
|
||||||
use async_zip::Compression;
|
use async_zip::Compression;
|
||||||
use async_zip::ZipEntryBuilder;
|
use async_zip::ZipEntryBuilder;
|
||||||
|
use async_zip::tokio::write::ZipFileWriter;
|
||||||
|
use axum::Json;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
|
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
|
||||||
use axum::http::{HeaderValue, StatusCode};
|
use axum::http::{HeaderValue, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::Json;
|
use log::{error, warn};
|
||||||
use include_dir::{include_dir, Dir};
|
|
||||||
use log::error;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::fs::write;
|
use tokio::fs::write;
|
||||||
use tokio::io::{copy, duplex, AsyncReadExt};
|
use tokio::io::{AsyncReadExt, copy, duplex};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tokio::sync::{oneshot, RwLock};
|
use tokio::sync::{RwLock, oneshot};
|
||||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
|
use crate::DiagDeviceCtrlMessage;
|
||||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::pcap::generate_pcap_data;
|
use crate::pcap::generate_pcap_data;
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
use crate::{display, DiagDeviceCtrlMessage};
|
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub config_path: String,
|
pub config_path: String,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
pub ui_update_sender: Sender<display::DisplayState>,
|
|
||||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||||
@@ -44,7 +42,7 @@ pub async fn get_qmdl(
|
|||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let (entry_index, entry) = qmdl_store.entry_for_name(qmdl_idx).ok_or((
|
let (entry_index, entry) = qmdl_store.entry_for_name(qmdl_idx).ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
format!("couldn't find qmdl file with name {}", qmdl_idx),
|
format!("couldn't find qmdl file with name {qmdl_idx}"),
|
||||||
))?;
|
))?;
|
||||||
let qmdl_file = qmdl_store
|
let qmdl_file = qmdl_store
|
||||||
.open_entry_qmdl(entry_index)
|
.open_entry_qmdl(entry_index)
|
||||||
@@ -52,7 +50,7 @@ pub async fn get_qmdl(
|
|||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("error opening QMDL file: {}", err),
|
format!("error opening QMDL file: {err}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||||
@@ -66,29 +64,45 @@ pub async fn get_qmdl(
|
|||||||
Ok((headers, body).into_response())
|
Ok((headers, body).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bundles the server's static files (html/css/js) into the binary for easy distribution
|
|
||||||
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/web/build");
|
|
||||||
|
|
||||||
pub async fn serve_static(
|
pub async fn serve_static(
|
||||||
State(_): State<Arc<ServerState>>,
|
State(_): State<Arc<ServerState>>,
|
||||||
Path(path): Path<String>,
|
Path(path): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let path = path.trim_start_matches('/');
|
let path = path.trim_start_matches('/');
|
||||||
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
|
||||||
|
|
||||||
match STATIC_DIR.get_file(path) {
|
match path {
|
||||||
None => Response::builder()
|
"rayhunter_icon.png" => (
|
||||||
.status(StatusCode::NOT_FOUND)
|
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
|
||||||
.body(Body::empty())
|
include_bytes!("../web/build/rayhunter_icon.png"),
|
||||||
.unwrap(),
|
)
|
||||||
Some(file) => Response::builder()
|
.into_response(),
|
||||||
.status(StatusCode::OK)
|
"rayhunter_orca_only.png" => (
|
||||||
.header(
|
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
|
||||||
header::CONTENT_TYPE,
|
include_bytes!("../web/build/rayhunter_orca_only.png"),
|
||||||
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
|
)
|
||||||
)
|
.into_response(),
|
||||||
.body(Body::from(file.contents()))
|
"rayhunter_text.png" => (
|
||||||
.unwrap(),
|
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
|
||||||
|
include_bytes!("../web/build/rayhunter_text.png"),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
"favicon.png" => (
|
||||||
|
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
|
||||||
|
include_bytes!("../web/build/favicon.png"),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
"index.html" => (
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, HeaderValue::from_static("text/html")),
|
||||||
|
(header::CONTENT_ENCODING, HeaderValue::from_static("gzip")),
|
||||||
|
],
|
||||||
|
include_bytes!("../web/build/index.html.gz"),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
path => {
|
||||||
|
warn!("404 on path: {path}");
|
||||||
|
StatusCode::NOT_FOUND.into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,14 +119,14 @@ pub async fn set_config(
|
|||||||
let config_str = toml::to_string_pretty(&config).map_err(|err| {
|
let config_str = toml::to_string_pretty(&config).map_err(|err| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("failed to serialize config as TOML: {}", err),
|
format!("failed to serialize config as TOML: {err}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
write(&state.config_path, config_str).await.map_err(|err| {
|
write(&state.config_path, config_str).await.map_err(|err| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("failed to write config file: {}", err),
|
format!("failed to write config file: {err}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -146,7 +160,7 @@ pub async fn get_zip(
|
|||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
format!("couldn't find entry with name {}", qmdl_idx),
|
format!("couldn't find entry with name {qmdl_idx}"),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
if entry.qmdl_size_bytes == 0 {
|
if entry.qmdl_size_bytes == 0 {
|
||||||
@@ -207,7 +221,7 @@ pub async fn get_zip(
|
|||||||
{
|
{
|
||||||
// if we fail to generate the PCAP file, we should still continue and give the
|
// if we fail to generate the PCAP file, we should still continue and give the
|
||||||
// user the QMDL.
|
// user the QMDL.
|
||||||
error!("Failed to generate PCAP: {:?}", e);
|
error!("Failed to generate PCAP: {e:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
entry_writer.into_inner().close().await?;
|
entry_writer.into_inner().close().await?;
|
||||||
@@ -219,7 +233,7 @@ pub async fn get_zip(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
error!("Error generating ZIP file: {:?}", e);
|
error!("Error generating ZIP file: {e:?}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,12 +292,11 @@ mod tests {
|
|||||||
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||||
) -> Arc<ServerState> {
|
) -> Arc<ServerState> {
|
||||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||||
let (ui_tx, _ui_rx) = tokio::sync::mpsc::channel(1);
|
|
||||||
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
|
||||||
let analysis_status = {
|
let analysis_status = {
|
||||||
let store = store_lock.try_read().unwrap();
|
let store = store_lock.try_read().unwrap();
|
||||||
crate::analysis::AnalysisStatus::new(&*store)
|
crate::analysis::AnalysisStatus::new(&store)
|
||||||
};
|
};
|
||||||
|
|
||||||
Arc::new(ServerState {
|
Arc::new(ServerState {
|
||||||
@@ -291,7 +304,6 @@ mod tests {
|
|||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
qmdl_store_lock: store_lock,
|
qmdl_store_lock: store_lock,
|
||||||
diag_device_ctrl_sender: tx,
|
diag_device_ctrl_sender: tx,
|
||||||
ui_update_sender: ui_tx,
|
|
||||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||||
analysis_sender: analysis_tx,
|
analysis_sender: analysis_tx,
|
||||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||||
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
|||||||
use crate::qmdl_store::ManifestEntry;
|
use crate::qmdl_store::ManifestEntry;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::Json;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use rayhunter::util::RuntimeMetadata;
|
use rayhunter::util::RuntimeMetadata;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -102,7 +102,7 @@ impl MemoryStats {
|
|||||||
// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M")
|
// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M")
|
||||||
fn humanize_kb(kb: usize) -> String {
|
fn humanize_kb(kb: usize) -> String {
|
||||||
if kb < 1000 {
|
if kb < 1000 {
|
||||||
return format!("{}K", kb);
|
return format!("{kb}K");
|
||||||
}
|
}
|
||||||
format!("{:.1}M", kb as f64 / 1024.0)
|
format!("{:.1}M", kb as f64 / 1024.0)
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ pub async fn get_system_stats(
|
|||||||
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
|
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
|
||||||
Ok(stats) => Ok(Json(stats)),
|
Ok(stats) => Ok(Json(stats)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("error getting system stats: {}", err);
|
error!("error getting system stats: {err}");
|
||||||
Err((
|
Err((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"error getting system stats".to_string(),
|
"error getting system stats".to_string(),
|
||||||
@@ -2,3 +2,6 @@
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Static Assets
|
||||||
|
static/pico.min.css
|
||||||
@@ -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,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,38 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build && gzip -9 ./build/index.html",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"test": "npm run test:unit -- --run",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"fix": "eslint --fix ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.5",
|
||||||
|
"@sveltejs/kit": "^2.13.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"@types/eslint": "^9.6.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.7.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
|
"globals": "^15.0.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.0.0",
|
||||||
|
"vite": "^5.0.3",
|
||||||
|
"vitest": "^2.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<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" class="m-4 xl:m-8">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { AnalysisRowType, EventType, parse_finished_report, Severity } from './analysis.svelte';
|
||||||
|
import { type NewlineDeliminatedJson } from './ndjson';
|
||||||
|
|
||||||
|
const SAMPLE_V1_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||||
|
{
|
||||||
|
analyzers: [
|
||||||
|
{
|
||||||
|
name: 'Analyzer 1',
|
||||||
|
description: 'A first analyzer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Analyzer 2',
|
||||||
|
description: 'A second analyzer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: '2024-10-08T13:25:43.011689003-07:00',
|
||||||
|
skipped_message_reasons: ['The reason why the message was skipped'],
|
||||||
|
analysis: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: '2024-10-08T13:25:43.480872496-07:00',
|
||||||
|
skipped_message_reasons: [],
|
||||||
|
analysis: [
|
||||||
|
{
|
||||||
|
timestamp: '2024-08-19T03:33:54.318Z',
|
||||||
|
events: [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
event_type: { type: 'QualitativeWarning', severity: 'Low' },
|
||||||
|
message: 'Something nasty happened',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||||
|
{
|
||||||
|
analyzers: [
|
||||||
|
{
|
||||||
|
name: 'Analyzer 1',
|
||||||
|
description: 'A first analyzer',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Analyzer 2',
|
||||||
|
description: 'A second analyzer',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
report_version: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipped_message_reason: 'The reason why the message was skipped',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packet_timestamp: '2024-08-19T03:33:54.318Z',
|
||||||
|
events: [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
event_type: { type: 'QualitativeWarning', severity: 'Low' },
|
||||||
|
message: 'Something nasty happened',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('analysis report parsing', () => {
|
||||||
|
it('parses v1 example analysis', () => {
|
||||||
|
const report = parse_finished_report(SAMPLE_V1_REPORT_NDJSON);
|
||||||
|
expect(report.metadata.report_version).toEqual(1);
|
||||||
|
expect(report.metadata.analyzers).toEqual([
|
||||||
|
{
|
||||||
|
name: 'Analyzer 1',
|
||||||
|
description: 'A first analyzer',
|
||||||
|
version: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Analyzer 2',
|
||||||
|
description: 'A second analyzer',
|
||||||
|
version: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(report.rows).toHaveLength(2);
|
||||||
|
expect(report.rows[0].type).toBe(AnalysisRowType.Skipped);
|
||||||
|
if (report.rows[1].type === AnalysisRowType.Analysis) {
|
||||||
|
const row = report.rows[1];
|
||||||
|
expect(row.events).toHaveLength(2);
|
||||||
|
expect(row.events[0]).toBeNull();
|
||||||
|
const event = row.events[1];
|
||||||
|
const expected_timestamp = new Date('2024-08-19T03:33:54.318Z');
|
||||||
|
expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime());
|
||||||
|
if (event !== null && event.type === EventType.Warning) {
|
||||||
|
expect(event.severity).toEqual(Severity.Low);
|
||||||
|
} else {
|
||||||
|
throw 'wrong event type';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw 'wrong row type';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses v2 example analysis', () => {
|
||||||
|
const report = parse_finished_report(SAMPLE_V2_REPORT_NDJSON);
|
||||||
|
expect(report.metadata.report_version).toEqual(2);
|
||||||
|
expect(report.metadata.analyzers).toEqual([
|
||||||
|
{
|
||||||
|
name: 'Analyzer 1',
|
||||||
|
description: 'A first analyzer',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Analyzer 2',
|
||||||
|
description: 'A second analyzer',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(report.rows).toHaveLength(2);
|
||||||
|
expect(report.rows[0].type).toBe(AnalysisRowType.Skipped);
|
||||||
|
if (report.rows[1].type === AnalysisRowType.Analysis) {
|
||||||
|
const row = report.rows[1];
|
||||||
|
expect(row.events).toHaveLength(2);
|
||||||
|
expect(row.events[0]).toBeNull();
|
||||||
|
const event = row.events[1];
|
||||||
|
const expected_timestamp = new Date('2024-08-19T03:33:54.318Z');
|
||||||
|
expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime());
|
||||||
|
if (event !== null && event.type === EventType.Warning) {
|
||||||
|
expect(event.severity).toEqual(Severity.Low);
|
||||||
|
} else {
|
||||||
|
throw 'wrong event type';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw 'wrong row type';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { parse_ndjson, type NewlineDeliminatedJson } from './ndjson';
|
||||||
|
import { req } from './utils.svelte';
|
||||||
|
|
||||||
|
export type AnalysisReport = {
|
||||||
|
metadata: ReportMetadata;
|
||||||
|
rows: AnalysisRow[];
|
||||||
|
statistics: ReportStatistics;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReportStatistics = {
|
||||||
|
num_warnings: number;
|
||||||
|
num_informational_logs: number;
|
||||||
|
num_skipped_packets: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReportMetadata {
|
||||||
|
public analyzers: AnalyzerMetadata[];
|
||||||
|
public rayhunter: RayhunterMetadata;
|
||||||
|
public report_version: number;
|
||||||
|
|
||||||
|
constructor(ndjson: any) {
|
||||||
|
this.analyzers = ndjson.analyzers;
|
||||||
|
this.rayhunter = ndjson.rayhunter;
|
||||||
|
if (ndjson.report_version === undefined) {
|
||||||
|
this.report_version = 1;
|
||||||
|
// we consider our legacy (unversioned) heuristics to be v0 --
|
||||||
|
// this'll let us clearly differentiate some known false-positive
|
||||||
|
// results from the pre-versioned era from v1 heuristics
|
||||||
|
this.analyzers.forEach((analyzer) => {
|
||||||
|
analyzer.version = 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.report_version = ndjson.report_version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RayhunterMetadata = {
|
||||||
|
rayhunter_version: string;
|
||||||
|
system_os: string;
|
||||||
|
arch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyzerMetadata = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalysisRow = SkippedPacket | PacketAnalysis;
|
||||||
|
export enum AnalysisRowType {
|
||||||
|
Skipped,
|
||||||
|
Analysis,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SkippedPacket = {
|
||||||
|
type: AnalysisRowType.Skipped;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PacketAnalysis = {
|
||||||
|
type: AnalysisRowType.Analysis;
|
||||||
|
packet_timestamp: Date;
|
||||||
|
events: Event[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Event = QualitativeWarning | InformationalEvent | null;
|
||||||
|
export enum EventType {
|
||||||
|
Informational,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QualitativeWarning = {
|
||||||
|
type: EventType.Warning;
|
||||||
|
severity: Severity;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Severity {
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InformationalEvent = {
|
||||||
|
type: EventType.Informational;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function get_event(event_json: any): Event {
|
||||||
|
if (event_json.event_type.type === 'Informational') {
|
||||||
|
return {
|
||||||
|
type: EventType.Informational,
|
||||||
|
message: event_json.message,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: EventType.Warning,
|
||||||
|
severity:
|
||||||
|
event_json.event_type.severity === 'High'
|
||||||
|
? Severity.High
|
||||||
|
: event_json.event_type.severity === 'Medium'
|
||||||
|
? Severity.Medium
|
||||||
|
: Severity.Low,
|
||||||
|
message: event_json.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_v1_rows(row_jsons: any[]): AnalysisRow[] {
|
||||||
|
const rows: AnalysisRow[] = [];
|
||||||
|
for (const row_json of row_jsons) {
|
||||||
|
for (const reason of row_json.skipped_message_reasons) {
|
||||||
|
rows.push({
|
||||||
|
type: AnalysisRowType.Skipped,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const analysis_json of row_json.analysis) {
|
||||||
|
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
||||||
|
if (event_json === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return get_event(event_json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rows.push({
|
||||||
|
type: AnalysisRowType.Analysis,
|
||||||
|
packet_timestamp: new Date(analysis_json.timestamp),
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_v2_rows(row_jsons: any[]): AnalysisRow[] {
|
||||||
|
const rows: AnalysisRow[] = [];
|
||||||
|
for (const row_json of row_jsons) {
|
||||||
|
if (row_json.skipped_message_reason) {
|
||||||
|
rows.push({
|
||||||
|
type: AnalysisRowType.Skipped,
|
||||||
|
reason: row_json.skipped_message_reason,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const events: Event[] = row_json.events.map((event_json: any): Event | null => {
|
||||||
|
if (event_json === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return get_event(event_json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rows.push({
|
||||||
|
type: AnalysisRowType.Analysis,
|
||||||
|
packet_timestamp: new Date(row_json.packet_timestamp),
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_report_stats(rows: AnalysisRow[]): ReportStatistics {
|
||||||
|
let num_warnings = 0;
|
||||||
|
let num_informational_logs = 0;
|
||||||
|
let num_skipped_packets = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.type === AnalysisRowType.Skipped) {
|
||||||
|
num_skipped_packets++;
|
||||||
|
} else {
|
||||||
|
for (const event of row.events) {
|
||||||
|
if (event !== null) {
|
||||||
|
if (event.type === EventType.Informational) {
|
||||||
|
num_informational_logs++;
|
||||||
|
} else {
|
||||||
|
num_warnings++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
num_warnings,
|
||||||
|
num_informational_logs,
|
||||||
|
num_skipped_packets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport {
|
||||||
|
const metadata = new ReportMetadata(report_json[0]);
|
||||||
|
let rows;
|
||||||
|
if (metadata.report_version === 1) {
|
||||||
|
rows = get_v1_rows(report_json.slice(1));
|
||||||
|
} else {
|
||||||
|
rows = get_v2_rows(report_json.slice(1));
|
||||||
|
}
|
||||||
|
const statistics = get_report_stats(rows);
|
||||||
|
return {
|
||||||
|
statistics,
|
||||||
|
metadata,
|
||||||
|
rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_report(name: string): Promise<AnalysisReport> {
|
||||||
|
const report_json = parse_ndjson(await req('GET', `/api/analysis-report/${name}`));
|
||||||
|
return parse_finished_report(report_json);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
import { get_report, type AnalysisReport } from './analysis.svelte';
|
||||||
import type { Manifest, ManifestEntry } from "./manifest.svelte";
|
import { req } from './utils.svelte';
|
||||||
import { req } from "./utils.svelte";
|
|
||||||
|
|
||||||
export enum AnalysisStatus {
|
export enum AnalysisStatus {
|
||||||
// rayhunter is currently analyzing this entry (note that this is distinct
|
// rayhunter is currently analyzing this entry (note that this is distinct
|
||||||
@@ -19,8 +18,8 @@ type AnalysisStatusJson = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AnalysisResult = {
|
export type AnalysisResult = {
|
||||||
name: string,
|
name: string;
|
||||||
status: AnalysisStatus,
|
status: AnalysisStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AnalysisManager {
|
export class AnalysisManager {
|
||||||
@@ -53,11 +52,13 @@ export class AnalysisManager {
|
|||||||
|
|
||||||
// fetch the analysis report
|
// fetch the analysis report
|
||||||
this.reports.delete(entry);
|
this.reports.delete(entry);
|
||||||
get_report(entry).then(report => {
|
get_report(entry)
|
||||||
this.reports.set(entry, report);
|
.then((report) => {
|
||||||
}).catch(err => {
|
this.reports.set(entry, report);
|
||||||
this.reports.set(entry, `Failed to get analysis: ${err}`);
|
})
|
||||||
});
|
.catch((err) => {
|
||||||
|
this.reports.set(entry, `Failed to get analysis: ${err}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AnalysisStatus } from '$lib/analysisManager.svelte';
|
||||||
|
import type { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
|
let {
|
||||||
|
entry,
|
||||||
|
onclick,
|
||||||
|
analysis_visible,
|
||||||
|
}: {
|
||||||
|
entry: ManifestEntry;
|
||||||
|
onclick: () => void;
|
||||||
|
analysis_visible: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let summary = $derived.by(() => {
|
||||||
|
if (entry.analysis_status === AnalysisStatus.Queued) {
|
||||||
|
return 'Queued...';
|
||||||
|
} else if (entry.analysis_status === AnalysisStatus.Running) {
|
||||||
|
return 'Running...';
|
||||||
|
} else if (entry.analysis_status === AnalysisStatus.Finished) {
|
||||||
|
if (entry.analysis_report === undefined) {
|
||||||
|
return 'Loading...';
|
||||||
|
} else if (typeof entry.analysis_report === 'string') {
|
||||||
|
return entry.analysis_report;
|
||||||
|
} else {
|
||||||
|
return `${entry.analysis_report.statistics.num_warnings} warnings`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ready = $derived.by(() => {
|
||||||
|
let finished = entry.analysis_status === AnalysisStatus.Finished;
|
||||||
|
let report_available = entry.analysis_report !== undefined;
|
||||||
|
return finished && report_available;
|
||||||
|
});
|
||||||
|
|
||||||
|
let button_class = $derived(ready ? 'text-blue-600 border rounded-full px-2' : '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="flex flex-row gap-1 lg:gap-2" disabled={!ready} {onclick}>
|
||||||
|
<span
|
||||||
|
class="{button_class} {(entry.get_num_warnings() || 0) < 1
|
||||||
|
? 'text-green-700 border-green-500 bg-green-200'
|
||||||
|
: 'text-red-700 border-red-500 bg-red-200'}">{summary}</span
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}"
|
||||||
|
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="m19 9-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AnalysisRowType, EventType, type AnalysisReport } from '$lib/analysis.svelte';
|
||||||
|
let {
|
||||||
|
report,
|
||||||
|
}: {
|
||||||
|
report: AnalysisReport;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeStyle: 'long',
|
||||||
|
dateStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const analyzers = report.metadata.analyzers;
|
||||||
|
|
||||||
|
const skipped_messages: Map<string, number> = $derived.by(() => {
|
||||||
|
let map = new Map();
|
||||||
|
for (const row of report.rows) {
|
||||||
|
if (row.type === AnalysisRowType.Skipped) {
|
||||||
|
let count = map.get(row.reason);
|
||||||
|
if (count === undefined) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
map.set(row.reason, count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-lg underline">Warnings and Informational Logs</p>
|
||||||
|
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||||
|
<p>Nothing to show!</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-scroll">
|
||||||
|
<table class="table-auto text-left">
|
||||||
|
<thead class="p-2">
|
||||||
|
<tr class="bg-gray-300">
|
||||||
|
<th class="p-2">Timestamp</th>
|
||||||
|
<th class="p-2">Heuristic</th>
|
||||||
|
<th class="p-2">Warning</th>
|
||||||
|
<th class="p-2">Severity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each report.rows as row}
|
||||||
|
{#if row.type === AnalysisRowType.Analysis}
|
||||||
|
{@const parsed_date = new Date(row.packet_timestamp)}
|
||||||
|
{#each row.events.filter((e) => e !== null) as event, i}
|
||||||
|
{@const analyzer = analyzers[i]}
|
||||||
|
<tr class="even:bg-gray-200 odd:bg-white">
|
||||||
|
{#if event.type === EventType.Warning}
|
||||||
|
{@const severity = ['Low', 'Medium', 'High'][
|
||||||
|
event.severity
|
||||||
|
]}
|
||||||
|
{@const severity_class = [
|
||||||
|
'bg-red-200',
|
||||||
|
'bg-red-400',
|
||||||
|
'bg-red-600',
|
||||||
|
][event.severity]}
|
||||||
|
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||||
|
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||||
|
<td class="p-2">{event.message}</td>
|
||||||
|
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||||
|
{:else if event.type === EventType.Informational}
|
||||||
|
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||||
|
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||||
|
<td class="p-2">{event.message}</td>
|
||||||
|
<td class="p-2">Info</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if report.statistics.num_skipped_packets > 0}
|
||||||
|
<div>
|
||||||
|
<p class="text-lg underline">Unparsed Messages</p>
|
||||||
|
<p>
|
||||||
|
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
||||||
|
problem.
|
||||||
|
</p>
|
||||||
|
<div class="overflow-x-scroll">
|
||||||
|
<table class="table-auto text-left">
|
||||||
|
<thead class="p-2">
|
||||||
|
<tr class="bg-gray-300">
|
||||||
|
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||||
|
<th scope="col">Reason/Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each skipped_messages.entries() as [message, count]}
|
||||||
|
<tr class="even:bg-gray-200 odd:bg-white">
|
||||||
|
<td class="text-center">{count}</td>
|
||||||
|
<td>{message}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type ReportMetadata } from '$lib/analysis.svelte';
|
||||||
|
import type { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
|
import AnalysisTable from './AnalysisTable.svelte';
|
||||||
|
let {
|
||||||
|
entry,
|
||||||
|
}: {
|
||||||
|
entry: ManifestEntry;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mt-2">
|
||||||
|
{#if entry.analysis_report === undefined}
|
||||||
|
<p>Report unavailable, try refreshing.</p>
|
||||||
|
{:else if typeof entry.analysis_report === 'string'}
|
||||||
|
<p>Error getting analysis report: {entry.analysis_report}</p>
|
||||||
|
{:else}
|
||||||
|
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#if entry.analysis_report.rows.length > 0}
|
||||||
|
<AnalysisTable report={entry.analysis_report} />
|
||||||
|
{:else}
|
||||||
|
<p>No warnings to display!</p>
|
||||||
|
{/if}
|
||||||
|
{#if metadata !== undefined && metadata.rayhunter !== undefined}
|
||||||
|
<div>
|
||||||
|
<p class="text-lg underline">Metadata</p>
|
||||||
|
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
||||||
|
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg underline">Analyzers</p>
|
||||||
|
{#each metadata.analyzers as analyzer}
|
||||||
|
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>N/A (analysis generated by an older version of rayhunter)</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -5,19 +5,19 @@
|
|||||||
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let message = $state("");
|
let message = $state('');
|
||||||
let messageType = $state<"success" | "error" | null>(null);
|
let messageType = $state<'success' | 'error' | null>(null);
|
||||||
let showConfig = $state(false);
|
let showConfig = $state(false);
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
config = await get_config();
|
config = await get_config();
|
||||||
message = "";
|
message = '';
|
||||||
messageType = null;
|
messageType = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message = `Failed to load config: ${error}`;
|
message = `Failed to load config: ${error}`;
|
||||||
messageType = "error";
|
messageType = 'error';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -25,21 +25,21 @@
|
|||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving = true;
|
saving = true;
|
||||||
await set_config(config);
|
await set_config(config);
|
||||||
message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.";
|
message =
|
||||||
messageType = "success";
|
'Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.';
|
||||||
|
messageType = 'success';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message = `Failed to save config: ${error}`;
|
message = `Failed to save config: ${error}`;
|
||||||
messageType = "error";
|
messageType = 'error';
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Load config when first shown
|
// Load config when first shown
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (showConfig && !config) {
|
if (showConfig && !config) {
|
||||||
@@ -49,21 +49,33 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 m-4">
|
<div class="bg-white rounded-lg shadow-md p-6 m-4">
|
||||||
<button
|
<button
|
||||||
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
|
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
|
||||||
onclick={() => showConfig = !showConfig}
|
onclick={() => (showConfig = !showConfig)}
|
||||||
>
|
>
|
||||||
<span>Configuration</span>
|
<span>Configuration</span>
|
||||||
<svg class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showConfig}
|
{#if showConfig}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="text-center py-4">Loading config...</div>
|
<div class="text-center py-4">Loading config...</div>
|
||||||
{:else if config}
|
{:else if config}
|
||||||
<form class="space-y-4" onsubmit={(e) => { e.preventDefault(); saveConfig(); }}>
|
<form
|
||||||
|
class="space-y-4"
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
saveConfig();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
|
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Device UI Level
|
Device UI Level
|
||||||
@@ -81,7 +93,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="key_input_mode" class="block text-sm font-medium text-gray-700 mb-1">
|
<label
|
||||||
|
for="key_input_mode"
|
||||||
|
class="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
Device Input Mode
|
Device Input Mode
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -90,7 +105,9 @@
|
|||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||||
>
|
>
|
||||||
<option value={0}>0 - Disable button control</option>
|
<option value={0}>0 - Disable button control</option>
|
||||||
<option value={1}>1 - Double-tap power button to start/stop recording</option>
|
<option value={1}
|
||||||
|
>1 - Double-tap power button to start/stop recording</option
|
||||||
|
>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,7 +126,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t pt-4 mt-6">
|
<div class="border-t pt-4 mt-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analyzer Heuristic Settings</h3>
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||||
|
Analyzer Heuristic Settings
|
||||||
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
@@ -130,7 +149,10 @@
|
|||||||
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
|
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
|
||||||
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="connection_redirect_2g_downgrade" class="ml-2 block text-sm text-gray-700">
|
<label
|
||||||
|
for="connection_redirect_2g_downgrade"
|
||||||
|
class="ml-2 block text-sm text-gray-700"
|
||||||
|
>
|
||||||
Connection Redirect 2G Downgrade Heuristic
|
Connection Redirect 2G Downgrade Heuristic
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +164,10 @@
|
|||||||
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
|
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
|
||||||
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="lte_sib6_and_7_downgrade" class="ml-2 block text-sm text-gray-700">
|
<label
|
||||||
|
for="lte_sib6_and_7_downgrade"
|
||||||
|
class="ml-2 block text-sm text-gray-700"
|
||||||
|
>
|
||||||
LTE SIB6 and SIB7 Downgrade Heuristic
|
LTE SIB6 and SIB7 Downgrade Heuristic
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,6 +183,30 @@
|
|||||||
Null Cipher Heuristic
|
Null Cipher Heuristic
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="nas_null_cipher"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={config.analyzers.nas_null_cipher}
|
||||||
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label for="nas_null_cipher" class="ml-2 block text-sm text-gray-700">
|
||||||
|
NAS Null Cipher Heuristic
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="incomplete_sib"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={config.analyzers.incomplete_sib}
|
||||||
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label for="nas_null_cipher" class="ml-2 block text-sm text-gray-700">
|
||||||
|
Incomplete SIB Heuristic
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,20 +217,35 @@
|
|||||||
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
||||||
>
|
>
|
||||||
{#if saving}
|
{#if saving}
|
||||||
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
<div
|
||||||
|
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||||
|
></div>
|
||||||
Saving...
|
Saving...
|
||||||
{:else}
|
{:else}
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
class="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
Apply and restart
|
Apply and restart
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{#if message}
|
{#if message}
|
||||||
<div class="mt-4 p-3 rounded {messageType === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}">
|
<div
|
||||||
|
class="mt-4 p-3 rounded {messageType === 'error'
|
||||||
|
? 'bg-red-100 text-red-700'
|
||||||
|
: 'bg-green-100 text-green-700'}"
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DeleteButton from './DeleteButton.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end gap-2">
|
||||||
|
<DeleteButton
|
||||||
|
text="Delete ALL Recordings"
|
||||||
|
prompt={`Are you sure you want to delete ALL recordings?`}
|
||||||
|
url={`/api/delete-all-recordings`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { req } from '$lib/utils.svelte';
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
url,
|
||||||
|
prompt,
|
||||||
|
}: {
|
||||||
|
text?: string;
|
||||||
|
url: string;
|
||||||
|
prompt: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
if (window.confirm(prompt)) {
|
||||||
|
req('POST', url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
|
||||||
|
onclick={confirmDelete}
|
||||||
|
aria-label="delete"
|
||||||
|
>
|
||||||
|
<p>{text}</p>
|
||||||
|
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="white"
|
||||||
|
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
url,
|
||||||
|
text,
|
||||||
|
full_button = false,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
text: string;
|
||||||
|
full_button?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex flex-row {full_button
|
||||||
|
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md'
|
||||||
|
: 'text-blue-600 underline'}"
|
||||||
|
onclick={download}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
<svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@@ -1,65 +1,87 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
||||||
import DeleteButton from "$lib/components/DeleteButton.svelte";
|
import DeleteButton from '$lib/components/DeleteButton.svelte';
|
||||||
import AnalysisStatus from "./AnalysisStatus.svelte";
|
import AnalysisStatus from './AnalysisStatus.svelte';
|
||||||
import AnalysisView from "./AnalysisView.svelte";
|
import AnalysisView from './AnalysisView.svelte';
|
||||||
import RecordingControls from "./RecordingControls.svelte";
|
import RecordingControls from './RecordingControls.svelte';
|
||||||
let { entry, current, i, server_is_recording }: {
|
let {
|
||||||
|
entry,
|
||||||
|
current,
|
||||||
|
server_is_recording,
|
||||||
|
}: {
|
||||||
entry: ManifestEntry;
|
entry: ManifestEntry;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
i: number;
|
|
||||||
server_is_recording: boolean;
|
server_is_recording: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// passing `undefined` as the locale uses the browser default
|
// passing `undefined` as the locale uses the browser default
|
||||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
timeStyle: "long",
|
timeStyle: 'long',
|
||||||
dateStyle: "short",
|
dateStyle: 'short',
|
||||||
});
|
});
|
||||||
let status_row_color = $derived.by(() => {
|
let status_row_color = $derived.by(() => {
|
||||||
const num_warnings = entry.get_num_warnings();
|
const num_warnings = entry.get_num_warnings();
|
||||||
if (num_warnings !== undefined && num_warnings > 0) {
|
if (num_warnings !== undefined && num_warnings > 0) {
|
||||||
return "bg-red-100";
|
return 'bg-red-100';
|
||||||
}
|
}
|
||||||
return current ? "bg-green-100" : "bg-gray-100"
|
return current ? 'bg-green-100' : 'bg-gray-100';
|
||||||
});
|
});
|
||||||
let status_border_color = $derived.by(() => {
|
let status_border_color = $derived.by(() => {
|
||||||
const num_warnings = entry.get_num_warnings();
|
const num_warnings = entry.get_num_warnings();
|
||||||
if (num_warnings !== undefined && num_warnings > 0) {
|
if (num_warnings !== undefined && num_warnings > 0) {
|
||||||
return "border-red-100";
|
return 'border-red-100';
|
||||||
}
|
}
|
||||||
return current ? "border-green-100" : "border-gray-100"
|
return current ? 'border-green-100' : 'border-gray-100';
|
||||||
});
|
});
|
||||||
let analysis_visible = $state(false);
|
let analysis_visible = $state(false);
|
||||||
function toggle_analysis_visibility() {
|
function toggle_analysis_visibility() {
|
||||||
analysis_visible = !analysis_visible;
|
analysis_visible = !analysis_visible;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<div class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1">
|
|
||||||
|
<div
|
||||||
|
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 overflow-x-scroll overflow-y-hidden"
|
||||||
|
>
|
||||||
{#if current}
|
{#if current}
|
||||||
<div class="flex flex-row justify-between gap-2">
|
<div class="flex flex-row justify-between gap-2">
|
||||||
<span class="text-xl mb-2">Current Recording</span>
|
<span class="text-xl mb-2">Current Recording</span>
|
||||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
<span class=""
|
||||||
|
><AnalysisStatus
|
||||||
|
onclick={toggle_analysis_visibility}
|
||||||
|
{entry}
|
||||||
|
{analysis_visible}
|
||||||
|
/></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<span class="font-bold">ID: {entry.name}</span>
|
<span class="font-bold">ID: {entry.name}</span>
|
||||||
{#if !current}
|
{#if !current}
|
||||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
<span class=""
|
||||||
|
><AnalysisStatus
|
||||||
|
onclick={toggle_analysis_visibility}
|
||||||
|
{entry}
|
||||||
|
{analysis_visible}
|
||||||
|
/></span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<span class="">{entry.get_readable_qmdl_size()}</span>
|
<span class="">{entry.get_readable_qmdl_size()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="">Start: {date_formatter.format(entry.start_time)}</span>
|
<span class="">Start: {date_formatter.format(entry.start_time)}</span>
|
||||||
<span class="">Last Message: {entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</span>
|
<span class=""
|
||||||
|
>Last Message: {(entry.last_message_time &&
|
||||||
|
date_formatter.format(entry.last_message_time)) ||
|
||||||
|
'N/A'}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
|
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-scroll">
|
||||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button=true />
|
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button=true />
|
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button=true />
|
<DownloadLink url={entry.get_zip_url()} text="zip" full_button />
|
||||||
{#if current}
|
{#if current}
|
||||||
<RecordingControls {server_is_recording} />
|
<RecordingControls {server_is_recording} />
|
||||||
{:else}
|
{:else}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
|
import TableRow from './ManifestTableRow.svelte';
|
||||||
|
import Card from './ManifestCard.svelte';
|
||||||
|
interface Props {
|
||||||
|
entries: ManifestEntry[];
|
||||||
|
server_is_recording: boolean;
|
||||||
|
}
|
||||||
|
let { entries, server_is_recording }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--For larger screens we use a table-->
|
||||||
|
<table class="hidden table-auto text-left lg:table">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-100 drop-shadow">
|
||||||
|
<th class="p-2" scope="col">ID</th>
|
||||||
|
<th class="p-2" scope="col">Started</th>
|
||||||
|
<th class="p-2" scope="col">Last Message</th>
|
||||||
|
<th class="p-2" scope="col">Size</th>
|
||||||
|
<th class="p-2" scope="col">PCAP</th>
|
||||||
|
<th class="p-2" scope="col">QMDL</th>
|
||||||
|
<th class="p-2" scope="col">ZIP</th>
|
||||||
|
<th class="p-2" scope="col">Analysis</th>
|
||||||
|
<th class="p-2" scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each entries as entry, i}
|
||||||
|
<TableRow {entry} current={false} {i} />
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--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} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
||||||
import DeleteButton from "$lib/components/DeleteButton.svelte";
|
import DeleteButton from '$lib/components/DeleteButton.svelte';
|
||||||
import AnalysisStatus from "./AnalysisStatus.svelte";
|
import AnalysisStatus from './AnalysisStatus.svelte';
|
||||||
import AnalysisView from "./AnalysisView.svelte";
|
import AnalysisView from './AnalysisView.svelte';
|
||||||
let { entry, current, i }: {
|
let {
|
||||||
|
entry,
|
||||||
|
current,
|
||||||
|
i,
|
||||||
|
}: {
|
||||||
entry: ManifestEntry;
|
entry: ManifestEntry;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
i: number;
|
i: number;
|
||||||
@@ -12,16 +16,16 @@
|
|||||||
|
|
||||||
// passing `undefined` as the locale uses the browser default
|
// passing `undefined` as the locale uses the browser default
|
||||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
timeStyle: "long",
|
timeStyle: 'long',
|
||||||
dateStyle: "short",
|
dateStyle: 'short',
|
||||||
});
|
});
|
||||||
let alternating_row_color = $derived(i % 2 == 0 ? "bg-white" : "bg-gray-100");
|
let alternating_row_color = $derived(i % 2 == 0 ? 'bg-white' : 'bg-gray-100');
|
||||||
let status_row_color = $derived.by(() => {
|
let status_row_color = $derived.by(() => {
|
||||||
const num_warnings = entry.get_num_warnings();
|
const num_warnings = entry.get_num_warnings();
|
||||||
if (num_warnings !== undefined && num_warnings > 0) {
|
if (num_warnings !== undefined && num_warnings > 0) {
|
||||||
return "bg-red-100";
|
return 'bg-red-100';
|
||||||
}
|
}
|
||||||
return current ? "bg-green-100" : alternating_row_color
|
return current ? 'bg-green-100' : alternating_row_color;
|
||||||
});
|
});
|
||||||
let analysis_visible = $state(false);
|
let analysis_visible = $state(false);
|
||||||
function toggle_analysis_visibility() {
|
function toggle_analysis_visibility() {
|
||||||
@@ -32,12 +36,16 @@
|
|||||||
<tr class="{status_row_color} drop-shadow">
|
<tr class="{status_row_color} drop-shadow">
|
||||||
<td class="p-2">{entry.name}</td>
|
<td class="p-2">{entry.name}</td>
|
||||||
<td class="p-2">{date_formatter.format(entry.start_time)}</td>
|
<td class="p-2">{date_formatter.format(entry.start_time)}</td>
|
||||||
<td class="p-2">{entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</td>
|
<td class="p-2"
|
||||||
|
>{(entry.last_message_time && date_formatter.format(entry.last_message_time)) || 'N/A'}</td
|
||||||
|
>
|
||||||
<td class="p-2">{entry.get_readable_qmdl_size()}</td>
|
<td class="p-2">{entry.get_readable_qmdl_size()}</td>
|
||||||
<td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td>
|
<td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td>
|
||||||
<td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td>
|
<td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td>
|
||||||
<td class="p-2"><DownloadLink url={entry.get_zip_url()} text="zip" /></td>
|
<td class="p-2"><DownloadLink url={entry.get_zip_url()} text="zip" /></td>
|
||||||
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></td>
|
<td class="p-2"
|
||||||
|
><AnalysisStatus onclick={toggle_analysis_visibility} {entry} {analysis_visible} /></td
|
||||||
|
>
|
||||||
{#if current}
|
{#if current}
|
||||||
<td class="p-2"></td>
|
<td class="p-2"></td>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { req } from '$lib/utils.svelte';
|
||||||
|
let {
|
||||||
|
server_is_recording,
|
||||||
|
}: {
|
||||||
|
server_is_recording: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let client_set_recording = $state(server_is_recording);
|
||||||
|
let waiting_for_server = $derived(client_set_recording !== server_is_recording);
|
||||||
|
|
||||||
|
async function start_recording() {
|
||||||
|
await req('POST', '/api/start-recording');
|
||||||
|
client_set_recording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop_recording() {
|
||||||
|
await req('POST', '/api/stop-recording');
|
||||||
|
client_set_recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recording_button_classes =
|
||||||
|
'text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row gap-1';
|
||||||
|
const stop_recording_classes = `${recording_button_classes} bg-red-500 opacity-50 cursor-not-allowed`;
|
||||||
|
const start_recording_classes = `${recording_button_classes} bg-blue-500 opacity-50 cursor-not-allowed`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if waiting_for_server}
|
||||||
|
<button
|
||||||
|
class={server_is_recording ? stop_recording_classes : start_recording_classes}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<span>{server_is_recording ? 'Stopping...' : 'Starting...'}</span>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-white animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else if server_is_recording}
|
||||||
|
<button
|
||||||
|
class="{recording_button_classes} bg-red-500 hover:bg-red-700"
|
||||||
|
onclick={stop_recording}
|
||||||
|
>
|
||||||
|
<span>Stop</span>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="{recording_button_classes} bg-blue-500 hover:bg-blue-700"
|
||||||
|
onclick={start_recording}
|
||||||
|
>
|
||||||
|
<span>Start</span>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-white"
|
||||||
|
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="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -1,34 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type SystemStats } from "$lib/systemStats";
|
import { type SystemStats } from '$lib/systemStats';
|
||||||
let { stats }: {
|
let {
|
||||||
|
stats,
|
||||||
|
}: {
|
||||||
stats: SystemStats;
|
stats: SystemStats;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const table_cell_classes = "border p-1 lg:p-2";
|
const table_cell_classes = 'border p-1 lg:p-2';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100">
|
<div
|
||||||
|
class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100"
|
||||||
|
>
|
||||||
<p class="text-xl mb-2">System Information</p>
|
<p class="text-xl mb-2">System Information</p>
|
||||||
<table class="table-auto border">
|
<table class="table-auto border">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border">
|
<tr class="border">
|
||||||
<th class={table_cell_classes}>
|
<th class={table_cell_classes}> Rayhunter Version </th>
|
||||||
Rayhunter Version
|
|
||||||
</th>
|
|
||||||
<td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
|
<td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="border">
|
<tr class="border">
|
||||||
<th class={table_cell_classes}>
|
<th class={table_cell_classes}> Storage </th>
|
||||||
Storage
|
|
||||||
</th>
|
|
||||||
<td class={table_cell_classes}>
|
<td class={table_cell_classes}>
|
||||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
|
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats
|
||||||
|
.disk_stats.available_size} available)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="border-b">
|
<tr class="border-b">
|
||||||
<th class={table_cell_classes}>
|
<th class={table_cell_classes}> Memory (RAM) </th>
|
||||||
Memory (RAM)
|
|
||||||
</th>
|
|
||||||
<td class={table_cell_classes}>
|
<td class={table_cell_classes}>
|
||||||
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
||||||
</td>
|
</td>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
import { get_report, type AnalysisReport } from './analysis.svelte';
|
||||||
import { AnalysisStatus, type AnalysisManager } from "./analysisManager.svelte";
|
import { AnalysisStatus, type AnalysisManager } from './analysisManager.svelte';
|
||||||
|
|
||||||
interface JsonManifest {
|
interface JsonManifest {
|
||||||
entries: JsonManifestEntry[];
|
entries: JsonManifestEntry[];
|
||||||
@@ -11,7 +11,6 @@ interface JsonManifestEntry {
|
|||||||
start_time: string;
|
start_time: string;
|
||||||
last_message_time: string;
|
last_message_time: string;
|
||||||
qmdl_size_bytes: number;
|
qmdl_size_bytes: number;
|
||||||
analysis_size_bytes: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Manifest {
|
export class Manifest {
|
||||||
@@ -19,7 +18,7 @@ export class Manifest {
|
|||||||
public current_entry: ManifestEntry | undefined;
|
public current_entry: ManifestEntry | undefined;
|
||||||
|
|
||||||
constructor(json: JsonManifest) {
|
constructor(json: JsonManifest) {
|
||||||
for (let entry of json.entries) {
|
for (const entry of json.entries) {
|
||||||
this.entries.push(new ManifestEntry(entry));
|
this.entries.push(new ManifestEntry(entry));
|
||||||
}
|
}
|
||||||
if (json.current_entry !== null) {
|
if (json.current_entry !== null) {
|
||||||
@@ -31,7 +30,7 @@ export class Manifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async set_analysis_status(manager: AnalysisManager) {
|
async set_analysis_status(manager: AnalysisManager) {
|
||||||
for (let entry of this.entries) {
|
for (const entry of this.entries) {
|
||||||
entry.analysis_status = manager.status.get(entry.name);
|
entry.analysis_status = manager.status.get(entry.name);
|
||||||
entry.analysis_report = manager.reports.get(entry.name);
|
entry.analysis_report = manager.reports.get(entry.name);
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ export class Manifest {
|
|||||||
if (this.current_entry) {
|
if (this.current_entry) {
|
||||||
try {
|
try {
|
||||||
this.current_entry.analysis_report = await get_report(this.current_entry.name);
|
this.current_entry.analysis_report = await get_report(this.current_entry.name);
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
|
this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,11 +46,11 @@ export class Manifest {
|
|||||||
// analysis report is always available
|
// analysis report is always available
|
||||||
this.current_entry.analysis_status = AnalysisStatus.Finished;
|
this.current_entry.analysis_status = AnalysisStatus.Finished;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ManifestEntry {
|
export class ManifestEntry {
|
||||||
public name = $state("");
|
public name = $state('');
|
||||||
public start_time: Date;
|
public start_time: Date;
|
||||||
public last_message_time: Date | undefined = $state(undefined);
|
public last_message_time: Date | undefined = $state(undefined);
|
||||||
public qmdl_size_bytes = $state(0);
|
public qmdl_size_bytes = $state(0);
|
||||||
@@ -62,7 +61,6 @@ export class ManifestEntry {
|
|||||||
constructor(json: JsonManifestEntry) {
|
constructor(json: JsonManifestEntry) {
|
||||||
this.name = json.name;
|
this.name = json.name;
|
||||||
this.qmdl_size_bytes = json.qmdl_size_bytes;
|
this.qmdl_size_bytes = json.qmdl_size_bytes;
|
||||||
this.analysis_size_bytes = json.analysis_size_bytes;
|
|
||||||
this.start_time = new Date(json.start_time);
|
this.start_time = new Date(json.start_time);
|
||||||
if (json.last_message_time) {
|
if (json.last_message_time) {
|
||||||
this.last_message_time = new Date(json.last_message_time);
|
this.last_message_time = new Date(json.last_message_time);
|
||||||
@@ -70,16 +68,16 @@ export class ManifestEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_readable_qmdl_size(): string {
|
get_readable_qmdl_size(): string {
|
||||||
if (this.qmdl_size_bytes === 0) return "0 Bytes";
|
if (this.qmdl_size_bytes === 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = 2 || 2;
|
const dm = 2;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
|
const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
|
||||||
return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_num_warnings(): number | undefined {
|
get_num_warnings(): number | undefined {
|
||||||
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
|
if (this.analysis_report === undefined || typeof this.analysis_report === 'string') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.analysis_report.statistics.num_warnings;
|
return this.analysis_report.statistics.num_warnings;
|
||||||
@@ -103,5 +101,5 @@ export class ManifestEntry {
|
|||||||
|
|
||||||
get_delete_url(): string {
|
get_delete_url(): string {
|
||||||
return `/api/delete-recording/${this.name}`;
|
return `/api/delete-recording/${this.name}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,32 +2,32 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { parse_ndjson } from './ndjson';
|
import { parse_ndjson } from './ndjson';
|
||||||
|
|
||||||
describe('parsing newline-deliminated json', () => {
|
describe('parsing newline-deliminated json', () => {
|
||||||
it('parses normal JSON', () => {
|
it('parses normal JSON', () => {
|
||||||
const json = JSON.stringify({ foo: 100 });
|
const json = JSON.stringify({ foo: 100 });
|
||||||
const result = parse_ndjson(json);
|
const result = parse_ndjson(json);
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toEqual({ foo: 100 });
|
expect(result[0]).toEqual({ foo: 100 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses simple newline-deliminated json', () => {
|
it('parses simple newline-deliminated json', () => {
|
||||||
const json_a = JSON.stringify({ a: 100 });
|
const json_a = JSON.stringify({ a: 100 });
|
||||||
const json_b = JSON.stringify({ b: 200 });
|
const json_b = JSON.stringify({ b: 200 });
|
||||||
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
expect(result[0]).toEqual({ a: 100 });
|
expect(result[0]).toEqual({ a: 100 });
|
||||||
expect(result[1]).toEqual({ b: 200 });
|
expect(result[1]).toEqual({ b: 200 });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('parses newline-deliminated json with escaped newlines within', () => {
|
it('parses newline-deliminated json with escaped newlines within', () => {
|
||||||
const json_a = JSON.stringify({ a: 'this one has\n newlines and\nstuff' });
|
const json_a = JSON.stringify({ a: 'this one has\n newlines and\nstuff' });
|
||||||
const json_b = JSON.stringify({ b: 200 });
|
const json_b = JSON.stringify({ b: 200 });
|
||||||
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
expect(result[0]).toEqual({ a: 'this one has\n newlines and\nstuff' });
|
expect(result[0]).toEqual({ a: 'this one has\n newlines and\nstuff' });
|
||||||
expect(result[1]).toEqual({ b: 200 });
|
expect(result[1]).toEqual({ b: 200 });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('actually errors out on invalid ndjson', () => {
|
it('actually errors out on invalid ndjson', () => {
|
||||||
expect(() => parse_ndjson("invalid\njson")).toThrow();
|
expect(() => parse_ndjson('invalid\njson')).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export interface SystemStats {
|
||||||
|
disk_stats: DiskStats;
|
||||||
|
memory_stats: MemoryStats;
|
||||||
|
runtime_metadata: RuntimeMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeMetadata {
|
||||||
|
rayhunter_version: string;
|
||||||
|
system_os: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiskStats {
|
||||||
|
partition: string;
|
||||||
|
total_size: string;
|
||||||
|
used_size: string;
|
||||||
|
available_size: string;
|
||||||
|
used_percent: string;
|
||||||
|
mounted_on: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryStats {
|
||||||
|
total: string;
|
||||||
|
used: string;
|
||||||
|
free: string;
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Manifest } from "./manifest.svelte";
|
import { Manifest } from './manifest.svelte';
|
||||||
import type { SystemStats } from "./systemStats";
|
import type { SystemStats } from './systemStats';
|
||||||
|
|
||||||
export interface AnalyzerConfig {
|
export interface AnalyzerConfig {
|
||||||
imsi_requested: boolean;
|
imsi_requested: boolean;
|
||||||
connection_redirect_2g_downgrade: boolean;
|
connection_redirect_2g_downgrade: boolean;
|
||||||
lte_sib6_and_7_downgrade: boolean;
|
lte_sib6_and_7_downgrade: boolean;
|
||||||
null_cipher: boolean;
|
null_cipher: boolean;
|
||||||
|
nas_null_cipher: boolean;
|
||||||
|
incomplete_sib: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@@ -46,9 +48,9 @@ export async function set_config(config: Config): Promise<void> {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||||
|
import { get_manifest, get_system_stats } from '$lib/utils.svelte';
|
||||||
|
import ManifestTable from '$lib/components/ManifestTable.svelte';
|
||||||
|
import Card from '$lib/components/ManifestCard.svelte';
|
||||||
|
import type { SystemStats } from '$lib/systemStats';
|
||||||
|
import { AnalysisManager } from '$lib/analysisManager.svelte';
|
||||||
|
import SystemStatsTable from '$lib/components/SystemStatsTable.svelte';
|
||||||
|
import DeleteAllButton from '$lib/components/DeleteAllButton.svelte';
|
||||||
|
import RecordingControls from '$lib/components//RecordingControls.svelte';
|
||||||
|
import ConfigForm from '$lib/components/ConfigForm.svelte';
|
||||||
|
|
||||||
|
let manager: AnalysisManager = new AnalysisManager();
|
||||||
|
let loaded = $state(false);
|
||||||
|
let entries: ManifestEntry[] = $state([]);
|
||||||
|
let current_entry: ManifestEntry | undefined = $state(undefined);
|
||||||
|
let system_stats: SystemStats | undefined = $state(undefined);
|
||||||
|
$effect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await manager.update();
|
||||||
|
let new_manifest = await get_manifest();
|
||||||
|
await new_manifest.set_analysis_status(manager);
|
||||||
|
entries = new_manifest.entries;
|
||||||
|
current_entry = new_manifest.current_entry;
|
||||||
|
|
||||||
|
system_stats = await get_system_stats();
|
||||||
|
loaded = true;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
<div class="m-4 xl:mx-8 flex flex-col gap-4">
|
||||||
|
{#if loaded}
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
|
{#if current_entry}
|
||||||
|
<Card entry={current_entry} current={true} server_is_recording={!!current_entry} />
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 justify-between"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-8 h-8 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
WARNING: Not Running
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
>Rayhunter is not currently running and will not detect abnormal behavior!</span
|
||||||
|
>
|
||||||
|
<div class="flex flex-row justify-end mt-2">
|
||||||
|
<RecordingControls server_is_recording={!!current_entry} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<SystemStatsTable stats={system_stats!} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-xl">History</span>
|
||||||
|
<ManifestTable {entries} server_is_recording={!!current_entry} />
|
||||||
|
</div>
|
||||||
|
<DeleteAllButton />
|
||||||
|
<ConfigForm />
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<!-- https://www.w3.org/WAI/tutorials/images/decorative/ -->
|
||||||
|
<img src="/rayhunter_orca_only.png" alt="" class="h-48 animate-spin" />
|
||||||
|
<p class="text-xl">Loading...</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,26 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
// default options are shown. On some platforms
|
||||||
|
// these options are set automatically — see below
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: undefined,
|
||||||
|
precompress: false,
|
||||||
|
strict: true,
|
||||||
|
}),
|
||||||
|
output: {
|
||||||
|
// Force everything into one HTML file. SvelteKit will still generate
|
||||||
|
// a lot of JS files but they are deadweight and will not be included
|
||||||
|
// in the rust binary.
|
||||||
|
bundleStrategy: 'inline',
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
// Use a deterministic version string for reproducible builds.
|
||||||
|
// Without this option, SvelteKit will use a timestamp.
|
||||||
|
name: process.env.GITHUB_SHA || 'dev',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'rayhunter-blue': '#4e4eb1',
|
||||||
|
'rayhunter-dark-blue': '#3f3da0',
|
||||||
|
'rayhunter-green': '#94ea18',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [],
|
||||||
|
} as Config;
|
||||||