mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58c60c2661 | |||
| e0ae8a0298 | |||
| e6a3a4331e | |||
| 9191540e86 | |||
| 0a93e93838 | |||
| 0580a8af33 | |||
| a80a985b40 | |||
| 228596ef30 | |||
| a7409b281b | |||
| 6a57bdebc4 | |||
| 7cb405c465 | |||
| bada3846dc | |||
| f0849340cf | |||
| 512cf784a7 | |||
| 100960bbe1 | |||
| 9d275e1793 | |||
| fd190c4b75 | |||
| ff838c41fa | |||
| a031e8ccfc | |||
| a6f5faa80e | |||
| 43f1dfce64 | |||
| 54adaf913d | |||
| ab418ecc84 | |||
| 2fd028dc78 | |||
| d413840c08 | |||
| 2f1b583e00 | |||
| adeeb75166 | |||
| 4ca23f37c3 | |||
| 15b80ecdd5 | |||
| c5de9b045a | |||
| 37283deddb | |||
| 49d7bbca34 | |||
| a4c32f49ae | |||
| ec30a9557c | |||
| a7d38730f5 | |||
| d9facdf6cb | |||
| 90f49f73c8 | |||
| 8aa45f4b53 | |||
| d8da6118da | |||
| 3e38f500a9 | |||
| 83664e23f2 | |||
| 44c7f31fec | |||
| 301107be6c | |||
| 7b97ffc01d | |||
| b72712faa2 | |||
| 05fdc0eee2 | |||
| 8fb27b08f9 | |||
| 062db87572 | |||
| 9b6c4cee0b | |||
| 9d50db40b9 | |||
| d41c4bba3e | |||
| 1d5ed54033 | |||
| 24e79aad9d | |||
| bc7dcc97c6 | |||
| 480b6f8681 | |||
| 0c624c2bc2 | |||
| ec6967e2a1 | |||
| 912f7dfeaa | |||
| 51f1a33e86 | |||
| 87c79bddf7 | |||
| 5efa12f358 | |||
| e77fe469da | |||
| ed8b1903f8 | |||
| 89d1d71ec9 | |||
| 9be35de90e | |||
| 8f9be746d3 | |||
| 1347e3107a | |||
| 715efc4b0d | |||
| 836ec2169d | |||
| 9128eefcfc | |||
| 4f3c7fb7a9 | |||
| 2d3824072d |
@@ -7,3 +7,4 @@
|
||||
dist/config.toml.in eol=lf
|
||||
dist/scripts/misc-daemon eol=lf
|
||||
dist/scripts/rayhunter_daemon eol=lf
|
||||
scripts/*.sh eol=lf
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
- [ ] Code has been linted and run through `cargo fmt`.
|
||||
- [ ] If any new functionality has been added, unit tests were also added.
|
||||
- [ ] [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md) has been read.
|
||||
|
||||
You must check one of:
|
||||
- [ ] No generative AI (including LLMs) tools were used to create this PR.
|
||||
- [ ] Generative AI was used to create this PR. I certify that I have read and understand the code, and *that all comments and descriptions were authored by myself* and are not the product of generative AI.
|
||||
|
||||
+61
-10
@@ -24,7 +24,7 @@ jobs:
|
||||
daemon_changed: ${{ steps.files_changed.outputs.daemon_count != '0' }}
|
||||
daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
|
||||
web_changed: ${{ steps.files_changed.outputs.web_count != '0' }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' || steps.files_changed.outputs.daemon_count != '0' }}
|
||||
installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }}
|
||||
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }}
|
||||
rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
|
||||
@@ -84,25 +84,25 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install mdBook
|
||||
run: |
|
||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
- name: Test mdBook
|
||||
run: mdbook test
|
||||
|
||||
mdbook_publish:
|
||||
name: Publish mdBook to Github Pages
|
||||
mdbook_build:
|
||||
name: Build mdBook for Github Pages
|
||||
needs: mdbook_test
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
permissions:
|
||||
pages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install mdBook
|
||||
run: |
|
||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
@@ -110,14 +110,11 @@ jobs:
|
||||
- name: Build mdBook
|
||||
run: mdbook build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: book
|
||||
path: book
|
||||
- name: Deploy to Github Pages
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
check_and_test:
|
||||
needs: files_changed
|
||||
@@ -583,3 +580,57 @@ jobs:
|
||||
rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip
|
||||
rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}.zip.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
openapi_build:
|
||||
if: needs.files_changed.outputs.docs_changed == 'true'
|
||||
needs:
|
||||
- files_changed
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build rayhunter-daemon openapi docs
|
||||
run: |
|
||||
mkdir -p daemon/web/build
|
||||
touch daemon/web/build/{favicon.png,index.html.gz,rayhunter_orca_only.png,rayhunter_text.png}
|
||||
cargo run --bin gen_api --features apidocs -- ./rayhunter-openapi.json
|
||||
- name: Make swagger folder
|
||||
run: |
|
||||
mkdir api-docs
|
||||
mv doc/swagger-ui.html api-docs/index.html
|
||||
mv rayhunter-openapi.json api-docs/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: api-docs
|
||||
|
||||
github_pages_publish:
|
||||
name: Upload new documentation to Github Pages
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
permissions:
|
||||
pages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
needs:
|
||||
- mdbook_build
|
||||
- openapi_build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Organize pages into directory
|
||||
run: cp -a api-docs book/
|
||||
- name: Upload pages
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: book
|
||||
- name: Deploy Github Pages
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -60,6 +60,14 @@ Otherwise:
|
||||
|
||||
If you have any questions [feel free to open a discussion or chat with us on Mattermost.](https://efforg.github.io/rayhunter/support-feedback-community.html)
|
||||
|
||||
### Policy regarding AI-generated contributions:
|
||||
|
||||
- Please refrain from submissions that you haven't thoroughly understood, reviewed, and tested.
|
||||
- Please disclose if your contribution was AI-generated
|
||||
- Descriptions and comments should be made by you
|
||||
|
||||
You can read our [full policy](https://www.eff.org/about/opportunities/volunteer/coding-with-eff) and some writing on [our motivations](https://www.eff.org/deeplinks/2026/02/effs-policy-llm-assisted-contributions-our-open-source-projects).
|
||||
|
||||
## Making releases
|
||||
|
||||
This one is for maintainers of Rayhunter.
|
||||
|
||||
Generated
+87
-38
@@ -274,6 +274,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
@@ -945,6 +957,23 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -1733,9 +1762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -1812,9 +1841,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1827,9 +1856,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1837,15 +1866,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1854,9 +1883,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1873,9 +1902,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1884,21 +1913,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1908,7 +1937,6 @@ dependencies = [
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -2725,7 +2753,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
@@ -2753,12 +2781,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer-gui"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"installer",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
@@ -4131,12 +4160,6 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
@@ -4449,9 +4472,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
@@ -4671,8 +4694,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -4690,11 +4714,12 @@ dependencies = [
|
||||
"telcom-parser",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-check"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
@@ -4707,7 +4732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4731,6 +4756,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml 0.8.22",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4895,7 +4921,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
]
|
||||
@@ -5951,7 +5977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
@@ -6318,9 +6344,9 @@ checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -6554,6 +6580,29 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
|
||||
dependencies = [
|
||||
"indexmap 2.12.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
|
||||
@@ -6,3 +6,4 @@ title = "Rayhunter - An IMSI Catcher Catcher"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://github.com/efforg/rayhunter/edit/main/{path}"
|
||||
additional-css = ["doc/custom.css"]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+5
-10
@@ -113,12 +113,8 @@ async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
|
||||
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 compressed = qmdl_path.ends_with(".gz");
|
||||
let qmdl_reader = QmdlReader::new(qmdl_file, compressed, None);
|
||||
let mut qmdl_stream = pin!(
|
||||
qmdl_reader
|
||||
.as_stream()
|
||||
@@ -141,8 +137,9 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
||||
let qmdl_file = &mut File::open(&qmdl_path)
|
||||
.await
|
||||
.expect("failed to open qmdl file");
|
||||
let compressed = qmdl_path.ends_with(".gz");
|
||||
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 qmdl_reader = QmdlReader::new(qmdl_file, compressed, 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)
|
||||
@@ -197,9 +194,7 @@ async fn main() {
|
||||
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") {
|
||||
if name_str.ends_with(".qmdl") || name_str.ends_with(".qmdl.gz") {
|
||||
info!("**** Beginning analysis of {name_str}");
|
||||
analyze_qmdl(path_str, args.show_skipped).await;
|
||||
if args.pcapify {
|
||||
|
||||
+14
-3
@@ -1,13 +1,23 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[lib]
|
||||
name = "rayhunter_daemon"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "gen_api"
|
||||
path = "src/bin/gen_api.rs"
|
||||
required-features = ["apidocs"]
|
||||
|
||||
[features]
|
||||
default = ["rustcrypto-tls"]
|
||||
rustcrypto-tls = ["reqwest/rustls-tls-webpki-roots-no-provider", "dep:rustls-rustcrypto"]
|
||||
ring-tls = ["reqwest/rustls-tls-webpki-roots"]
|
||||
apidocs = ["dep:utoipa"]
|
||||
|
||||
[dependencies]
|
||||
rayhunter = { path = "../lib" }
|
||||
@@ -23,12 +33,13 @@ futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = { version = "0.1.14", default-features = false, features = ["io-util"] }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
futures = { version = "0.3.32", default-features = false, features = ["std"] }
|
||||
serde_json = "1.0.114"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||
tempfile = "3.10.1"
|
||||
tempfile = "3.10.2"
|
||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||
anyhow = "1.0.98"
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
async-trait = "0.1.88"
|
||||
utoipa = { version = "5.4.0", optional = true }
|
||||
|
||||
+32
-10
@@ -10,7 +10,6 @@ use futures::TryStreamExt;
|
||||
use log::{error, info};
|
||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use serde::Serialize;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||
@@ -77,10 +76,15 @@ impl AnalysisWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// The system status relating to QMDL file analysis
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct AnalysisStatus {
|
||||
/// The vector array of queued files
|
||||
queued: Vec<String>,
|
||||
/// The file currently being analyzed
|
||||
running: Option<String>,
|
||||
/// The vector array of finished files
|
||||
finished: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -130,7 +134,7 @@ async fn perform_analysis(
|
||||
analyzer_config: &AnalyzerConfig,
|
||||
) -> Result<(), String> {
|
||||
info!("Opening QMDL and analysis file for {name}...");
|
||||
let (analysis_file, qmdl_file) = {
|
||||
let (analysis_file, qmdl_reader) = {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (entry_index, _) = qmdl_store
|
||||
.entry_for_name(name)
|
||||
@@ -139,23 +143,17 @@ async fn perform_analysis(
|
||||
.clear_and_open_entry_analysis(entry_index)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
let qmdl_file = qmdl_store
|
||||
let qmdl_reader = qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
|
||||
(analysis_file, qmdl_file)
|
||||
(analysis_file, qmdl_reader)
|
||||
};
|
||||
|
||||
let mut analysis_writer = AnalysisWriter::new(analysis_file, analyzer_config)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
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::pin!(
|
||||
qmdl_reader
|
||||
.as_stream()
|
||||
@@ -215,6 +213,16 @@ pub fn run_analysis_thread(
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/analysis",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = AnalysisStatus)
|
||||
),
|
||||
summary = "Analysis status",
|
||||
description = "Show analysis status for all QMDL files."
|
||||
))]
|
||||
pub async fn get_analysis_status(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<AnalysisStatus>, (StatusCode, String)> {
|
||||
@@ -231,6 +239,20 @@ fn queue_qmdl(name: &str, analysis_status: &mut RwLockWriteGuard<AnalysisStatus>
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/analysis/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Unable to queue analysis file")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL file to analyze")
|
||||
),
|
||||
summary = "Start analysis",
|
||||
description = "Begin analysis of QMDL file {name}."
|
||||
))]
|
||||
pub async fn start_analysis(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
|
||||
@@ -18,9 +18,13 @@ pub mod wingtech;
|
||||
|
||||
const LOW_BATTERY_LEVEL: u8 = 10;
|
||||
|
||||
/// Device battery information
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct BatteryState {
|
||||
/// The current level in percentage of the device battery
|
||||
level: u8,
|
||||
/// A boolean indicating whether the battery is currently being charged
|
||||
is_plugged_in: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let content = rayhunter_daemon::ApiDocs::generate();
|
||||
let mut filename = "openapi.json".to_string();
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() > 1 {
|
||||
filename = args[1].to_string();
|
||||
}
|
||||
|
||||
fs::write(filename, content).unwrap();
|
||||
}
|
||||
@@ -7,19 +7,33 @@ use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::notifications::NotificationType;
|
||||
|
||||
/// The structure of a valid rayhunter configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct Config {
|
||||
/// Path to store QMDL files
|
||||
pub qmdl_store_path: String,
|
||||
/// Listening port
|
||||
pub port: u16,
|
||||
/// Debug mode
|
||||
pub debug_mode: bool,
|
||||
/// Internal device name
|
||||
pub device: Device,
|
||||
/// UI level
|
||||
pub ui_level: u8,
|
||||
/// Colorblind mode
|
||||
pub colorblind_mode: bool,
|
||||
/// Key input mode
|
||||
pub key_input_mode: u8,
|
||||
/// ntfy.sh URL
|
||||
pub ntfy_url: Option<String>,
|
||||
/// Vector containing the types of enabled notifications
|
||||
pub enabled_notifications: Vec<NotificationType>,
|
||||
/// Vector containing the list of enabled analyzers
|
||||
pub analyzers: AnalyzerConfig,
|
||||
pub min_space_to_start_recording_mb: u64,
|
||||
pub min_space_to_continue_recording_mb: u64,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -35,6 +49,8 @@ impl Default for Config {
|
||||
analyzers: AnalyzerConfig::default(),
|
||||
ntfy_url: None,
|
||||
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
min_space_to_start_recording_mb: 1,
|
||||
min_space_to_continue_recording_mb: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+269
-41
@@ -17,6 +17,8 @@ use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
#[cfg(feature = "apidocs")]
|
||||
use rayhunter::analysis::analyzer::ReportMetadata;
|
||||
use rayhunter::analysis::analyzer::{AnalysisLineNormalizer, AnalyzerConfig, EventType};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
@@ -27,10 +29,15 @@ use crate::display;
|
||||
use crate::notifications::{Notification, NotificationType};
|
||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||
use crate::server::ServerState;
|
||||
use crate::stats::DiskStats;
|
||||
|
||||
const DISK_CHECK_BYTES_INTERVAL: usize = 256 * 1024;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
StartRecording,
|
||||
StartRecording {
|
||||
response_tx: Option<oneshot::Sender<Result<(), String>>>,
|
||||
},
|
||||
DeleteEntry {
|
||||
name: String,
|
||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||
@@ -46,48 +53,115 @@ pub struct DiagTask {
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
state: DiagState,
|
||||
max_type_seen: EventType,
|
||||
bytes_since_space_check: usize,
|
||||
low_space_warned: bool,
|
||||
}
|
||||
|
||||
enum DiagState {
|
||||
Recording {
|
||||
qmdl_writer: QmdlWriter<File>,
|
||||
qmdl_writer: Box<QmdlWriter<File>>,
|
||||
analysis_writer: Box<AnalysisWriter>,
|
||||
},
|
||||
Stopped,
|
||||
}
|
||||
|
||||
enum DiskSpaceCheck {
|
||||
Ok(u64),
|
||||
Warning(u64),
|
||||
Critical(u64),
|
||||
Failed,
|
||||
}
|
||||
|
||||
fn check_disk_space(path: &std::path::Path, warning_mb: u64, critical_mb: u64) -> DiskSpaceCheck {
|
||||
match DiskStats::new(path.to_str().unwrap()) {
|
||||
Ok(stats) => {
|
||||
let available_mb = stats.available_bytes.unwrap_or(0) / 1024 / 1024;
|
||||
if available_mb < critical_mb {
|
||||
DiskSpaceCheck::Critical(available_mb)
|
||||
} else if available_mb < warning_mb {
|
||||
DiskSpaceCheck::Warning(available_mb)
|
||||
} else {
|
||||
DiskSpaceCheck::Ok(available_mb)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to check disk space: {e}");
|
||||
DiskSpaceCheck::Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagTask {
|
||||
fn new(
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui_update_sender,
|
||||
analysis_sender,
|
||||
analyzer_config,
|
||||
notification_channel,
|
||||
min_space_to_start_mb,
|
||||
min_space_to_continue_mb,
|
||||
state: DiagState::Stopped,
|
||||
max_type_seen: EventType::Informational,
|
||||
bytes_since_space_check: 0,
|
||||
low_space_warned: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start recording
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
/// Start recording, returning an error if disk space is too low.
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) -> Result<(), String> {
|
||||
self.max_type_seen = EventType::Informational;
|
||||
let (qmdl_file, analysis_file) = qmdl_store
|
||||
.new_entry()
|
||||
.await
|
||||
.expect("failed creating QMDL file entry");
|
||||
self.bytes_since_space_check = 0;
|
||||
self.low_space_warned = false;
|
||||
|
||||
match check_disk_space(
|
||||
&qmdl_store.path,
|
||||
self.min_space_to_start_mb,
|
||||
self.min_space_to_continue_mb,
|
||||
) {
|
||||
DiskSpaceCheck::Critical(mb) | DiskSpaceCheck::Warning(mb) => {
|
||||
let msg = format!(
|
||||
"Insufficient disk space: {}MB available, {}MB required",
|
||||
mb, self.min_space_to_start_mb
|
||||
);
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
DiskSpaceCheck::Ok(mb) => {
|
||||
info!("Starting recording with {}MB disk space available", mb);
|
||||
}
|
||||
DiskSpaceCheck::Failed => {}
|
||||
}
|
||||
|
||||
let (qmdl_gz_file, analysis_file) = match qmdl_store.new_entry().await {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
let msg = format!("failed creating QMDL file entry: {e}");
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
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");
|
||||
let qmdl_writer = Box::new(QmdlWriter::new(qmdl_gz_file));
|
||||
let analysis_writer = match AnalysisWriter::new(analysis_file, &self.analyzer_config).await
|
||||
{
|
||||
Ok(writer) => Box::new(writer),
|
||||
Err(e) => {
|
||||
let msg = format!("failed to create analysis writer: {e}");
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
self.state = DiagState::Recording {
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
@@ -99,11 +173,17 @@ impl DiagTask {
|
||||
{
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop recording
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
/// Stop recording, optionally annotating the entry with a reason.
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore, reason: Option<String>) {
|
||||
self.stop_current_recording().await;
|
||||
if let Some(reason) = reason
|
||||
&& let Err(e) = qmdl_store.set_current_stop_reason(reason).await
|
||||
{
|
||||
warn!("couldn't set stop reason: {e}");
|
||||
}
|
||||
if let Some((_, entry)) = qmdl_store.get_current_entry()
|
||||
&& let Err(e) = self
|
||||
.analysis_sender
|
||||
@@ -132,7 +212,7 @@ impl DiagTask {
|
||||
name: &str,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
if qmdl_store.is_current_entry(name) {
|
||||
self.stop(qmdl_store).await;
|
||||
self.stop(qmdl_store, None).await;
|
||||
}
|
||||
let res = qmdl_store.delete_entry(name).await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
@@ -145,7 +225,7 @@ impl DiagTask {
|
||||
&mut self,
|
||||
qmdl_store: &mut RecordingStore,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
self.stop(qmdl_store).await;
|
||||
self.stop(qmdl_store, None).await;
|
||||
let res = qmdl_store.delete_all_entries().await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
error!("Error deleting QMDL entries {e}");
|
||||
@@ -157,13 +237,23 @@ impl DiagTask {
|
||||
let mut state = DiagState::Stopped;
|
||||
std::mem::swap(&mut self.state, &mut state);
|
||||
if let DiagState::Recording {
|
||||
analysis_writer, ..
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
analysis_writer
|
||||
.close()
|
||||
.await
|
||||
.expect("failed to close analysis writer");
|
||||
match (qmdl_writer.close().await, analysis_writer.close().await) {
|
||||
(Ok(()), Ok(())) => {}
|
||||
(qmdl_result, analysis_result) => {
|
||||
if let Err(err) = qmdl_result {
|
||||
error!("failed to close QmdlWriter: {:?}", err);
|
||||
}
|
||||
if let Err(err) = analysis_result {
|
||||
error!("failed to close AnalysisWriter: {:?}", err);
|
||||
}
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,26 +273,82 @@ impl DiagTask {
|
||||
analysis_writer,
|
||||
} = &mut self.state
|
||||
{
|
||||
qmdl_writer
|
||||
.write_container(&container)
|
||||
.await
|
||||
.expect("failed to write to QMDL writer");
|
||||
if self.bytes_since_space_check >= DISK_CHECK_BYTES_INTERVAL {
|
||||
self.bytes_since_space_check = 0;
|
||||
match check_disk_space(
|
||||
&qmdl_store.path,
|
||||
self.min_space_to_start_mb,
|
||||
self.min_space_to_continue_mb,
|
||||
) {
|
||||
DiskSpaceCheck::Critical(mb) => {
|
||||
let reason = format!(
|
||||
"Disk space critically low ({}MB free), recording stopped automatically",
|
||||
mb
|
||||
);
|
||||
error!("{reason}");
|
||||
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
reason.clone(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
DiskSpaceCheck::Warning(mb) => {
|
||||
if !self.low_space_warned {
|
||||
self.low_space_warned = true;
|
||||
warn!("Disk space low: {}MB remaining", mb);
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
format!("Disk space low: {}MB free", mb),
|
||||
Some(Duration::from_secs(30)),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = qmdl_writer.write_container(&container).await {
|
||||
let reason = format!("failed to write to QMDL (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
debug!(
|
||||
"total QMDL bytes written: {}, updating manifest...",
|
||||
qmdl_writer.total_written
|
||||
qmdl_writer.total_uncompressed_bytes
|
||||
);
|
||||
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)
|
||||
if let Err(e) = qmdl_store
|
||||
.update_entry_qmdl_size(index, qmdl_writer.total_uncompressed_bytes)
|
||||
.await
|
||||
.expect("failed to update qmdl file size");
|
||||
{
|
||||
let reason = format!("failed to update manifest (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
debug!("done!");
|
||||
let max_type = analysis_writer
|
||||
.analyze(container)
|
||||
.await
|
||||
.expect("failed to analyze container");
|
||||
let container_bytes: usize = container.messages.iter().map(|m| m.data.len()).sum();
|
||||
self.bytes_since_space_check += container_bytes;
|
||||
let max_type = match analysis_writer.analyze(container).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("failed to analyze container: {e}");
|
||||
EventType::Informational
|
||||
}
|
||||
};
|
||||
|
||||
if max_type > EventType::Informational {
|
||||
info!("a heuristic triggered on this run!");
|
||||
@@ -244,25 +390,30 @@ pub fn run_diag_read_thread(
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
) {
|
||||
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, notification_channel);
|
||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config, notification_channel, min_space_to_start_mb, min_space_to_continue_mb);
|
||||
qmdl_file_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
.await
|
||||
.unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = qmdl_file_rx.recv() => {
|
||||
match msg {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording { response_tx }) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
diag_task.start(qmdl_store.deref_mut()).await;
|
||||
let result = diag_task.start(qmdl_store.deref_mut()).await;
|
||||
if let Some(tx) = response_tx {
|
||||
tx.send(result).ok();
|
||||
}
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
diag_task.stop(qmdl_store.deref_mut()).await;
|
||||
diag_task.stop(qmdl_store.deref_mut(), None).await;
|
||||
},
|
||||
// None means all the Senders have been dropped, so it's
|
||||
// time to go
|
||||
@@ -305,6 +456,18 @@ pub fn run_diag_read_thread(
|
||||
}
|
||||
|
||||
/// Start recording API for web thread
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/start-recording",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::FORBIDDEN, description = "System is in debug mode"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Recording action unsuccessful")
|
||||
),
|
||||
summary = "Start recording",
|
||||
description = "Begin a new data capture."
|
||||
))]
|
||||
pub async fn start_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -312,9 +475,12 @@ pub async fn start_recording(
|
||||
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::StartRecording)
|
||||
.send(DiagDeviceCtrlMessage::StartRecording {
|
||||
response_tx: Some(response_tx),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -323,10 +489,29 @@ pub async fn start_recording(
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
match response_rx.await {
|
||||
Ok(Ok(())) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||
Ok(Err(reason)) => Err((StatusCode::INSUFFICIENT_STORAGE, reason)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to receive start recording response: {e}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop recording API for web thread
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/stop-recording",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::FORBIDDEN, description = "System is in debug mode"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Recording action unsuccessful")
|
||||
),
|
||||
summary = "Stop recording",
|
||||
description = "Stop current data capture."
|
||||
))]
|
||||
pub async fn stop_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -346,6 +531,22 @@ pub async fn stop_recording(
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/delete-recording/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::FORBIDDEN, description = "System is in debug mode"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Delete action unsuccessful"),
|
||||
(status = StatusCode::BAD_REQUEST, description = "Bad recording name or no such recording")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL file to delete")
|
||||
),
|
||||
summary = "Delete recording",
|
||||
description = "Remove data capture file named {name}."
|
||||
))]
|
||||
pub async fn delete_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
@@ -385,6 +586,18 @@ pub async fn delete_recording(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/delete-all-recordings",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::FORBIDDEN, description = "System is in debug mode"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Delete action unsuccessful")
|
||||
),
|
||||
summary = "Delete all recordings",
|
||||
description = "Remove all saved data capture files."
|
||||
))]
|
||||
pub async fn delete_all_recordings(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -416,6 +629,21 @@ pub async fn delete_all_recordings(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/analysis-report/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = ReportMetadata, content_type = "application/x-ndjson"),
|
||||
(status = StatusCode::SERVICE_UNAVAILABLE, description = "No QMDL files available; start a new recording."),
|
||||
(status = StatusCode::NOT_FOUND, description = "File {name} not found")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL file to analyze")
|
||||
),
|
||||
summary = "Analysis report",
|
||||
description = "Download processed analysis report for QMDL file {name}, as well as the types (and versions) of analyzers used."
|
||||
))]
|
||||
pub async fn get_analysis_report(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
|
||||
@@ -102,7 +102,7 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
resized_img = img;
|
||||
}
|
||||
let img_rgba8 = resized_img.as_rgba8().unwrap();
|
||||
let mut buf = Vec::new();
|
||||
let mut buf = Vec::with_capacity((height * width).try_into().unwrap());
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let px = img_rgba8.get_pixel(x, y);
|
||||
@@ -145,7 +145,7 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
|
||||
async fn draw_patterned_line(&mut self, color: Color, height: u32, pattern: LinePattern) {
|
||||
let width = self.dimensions().width;
|
||||
let mut buffer = Vec::new();
|
||||
let mut buffer = Vec::with_capacity((height * width).try_into().unwrap());
|
||||
|
||||
for _row in 0..height {
|
||||
for col in 0..width {
|
||||
|
||||
@@ -12,7 +12,9 @@ pub mod tplink_onebit;
|
||||
pub mod uz801;
|
||||
pub mod wingtech;
|
||||
|
||||
/// A list of available display states
|
||||
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub enum DisplayState {
|
||||
/// We're recording but no warning has been found yet.
|
||||
Recording,
|
||||
|
||||
@@ -23,7 +23,7 @@ impl GenericFramebuffer for Framebuffer {
|
||||
}
|
||||
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
let mut raw_buffer = Vec::with_capacity(buffer.len() * 2);
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
|
||||
@@ -50,7 +50,7 @@ impl GenericFramebuffer for Framebuffer {
|
||||
rop: 0,
|
||||
};
|
||||
|
||||
let mut raw_buffer = Vec::new();
|
||||
let mut raw_buffer = Vec::with_capacity(buffer.len() * 2);
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
|
||||
@@ -28,7 +28,7 @@ impl GenericFramebuffer for Framebuffer {
|
||||
}
|
||||
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
let mut raw_buffer = Vec::with_capacity(buffer.len() * 2);
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
|
||||
@@ -81,8 +81,9 @@ pub fn run_key_input_thread(
|
||||
{
|
||||
error!("Failed to send StopRecording: {e}");
|
||||
}
|
||||
if let Err(e) =
|
||||
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
|
||||
if let Err(e) = diag_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
.await
|
||||
{
|
||||
error!("Failed to send StartRecording: {e}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
pub mod analysis;
|
||||
pub mod battery;
|
||||
pub mod config;
|
||||
pub mod diag;
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
pub mod key_input;
|
||||
pub mod notifications;
|
||||
pub mod pcap;
|
||||
pub mod qmdl_store;
|
||||
pub mod server;
|
||||
pub mod stats;
|
||||
|
||||
#[cfg(feature = "apidocs")]
|
||||
use utoipa::OpenApi;
|
||||
|
||||
// Add anotated paths to api docs
|
||||
#[cfg(feature = "apidocs")]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
description = "OpenAPI documentation for Rayhunter daemon\n\n**Note:** API endpoints are subject to change as needs arise, though we will try to keep them as stable as possible and notify about breaking changes in the changelogs for new versions.\n\nNo endpoints require any authentication. To use the in-browser execution on this page, you may need to disable CORS temporarily for your browser.",
|
||||
license(
|
||||
name = "GNU General Public License v3.0",
|
||||
url = "https://github.com/EFForg/rayhunter/blob/main/LICENSE"
|
||||
)
|
||||
),
|
||||
paths(
|
||||
pcap::get_pcap,
|
||||
server::get_qmdl,
|
||||
server::get_zip,
|
||||
stats::get_system_stats,
|
||||
stats::get_qmdl_manifest,
|
||||
stats::get_log,
|
||||
diag::start_recording,
|
||||
diag::stop_recording,
|
||||
diag::delete_recording,
|
||||
diag::delete_all_recordings,
|
||||
diag::get_analysis_report,
|
||||
analysis::get_analysis_status,
|
||||
analysis::start_analysis,
|
||||
server::get_config,
|
||||
server::set_config,
|
||||
server::test_notification,
|
||||
server::get_time,
|
||||
server::set_time_offset,
|
||||
server::debug_set_display_state
|
||||
),
|
||||
servers(
|
||||
(
|
||||
url = "http://localhost:8080",
|
||||
description = "ADB port bridge"
|
||||
),
|
||||
(
|
||||
url = "http://192.168.1.1:8080",
|
||||
description = "Orbic WiFi GUI"
|
||||
),
|
||||
(
|
||||
url = "http://192.168.0.1:8080",
|
||||
description = "TPLink WiFi GUI"
|
||||
),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDocs;
|
||||
|
||||
#[cfg(feature = "apidocs")]
|
||||
impl ApiDocs {
|
||||
pub fn generate() -> String {
|
||||
ApiDocs::openapi().to_pretty_json().unwrap()
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,8 @@ async fn run_with_config(
|
||||
analysis_tx.clone(),
|
||||
config.analyzers.clone(),
|
||||
notification_service.new_handler(),
|
||||
config.min_space_to_start_recording_mb,
|
||||
config.min_space_to_continue_recording_mb,
|
||||
);
|
||||
info!("Starting UI");
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ pub enum NotificationError {
|
||||
HttpError(reqwest::StatusCode),
|
||||
}
|
||||
|
||||
/// Enum of valid notification types
|
||||
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub enum NotificationType {
|
||||
Warning,
|
||||
LowBattery,
|
||||
|
||||
+20
-13
@@ -1,4 +1,4 @@
|
||||
use crate::ServerState;
|
||||
use crate::server::ServerState;
|
||||
|
||||
use anyhow::Error;
|
||||
use axum::body::Body;
|
||||
@@ -18,6 +18,21 @@ use tokio_util::io::ReaderStream;
|
||||
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
||||
// written so far. This is done by spawning a thread which streams chunks of
|
||||
// pcap data to a channel that's piped to the client.
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/pcap/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "PCAP conversion successful", content_type = "application/vnd.tcpdump.pcap"),
|
||||
(status = StatusCode::NOT_FOUND, description = "Could not find file {name}"),
|
||||
(status = StatusCode::SERVICE_UNAVAILABLE, description = "QMDL file is empty")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL filename to convert and download")
|
||||
),
|
||||
summary = "Download a PCAP file",
|
||||
description = "Stream a PCAP file to a client in chunks by converting the QMDL data for file {name} written so far."
|
||||
))]
|
||||
pub async fn get_pcap(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(mut qmdl_name): Path<String>,
|
||||
@@ -30,23 +45,20 @@ pub async fn get_pcap(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find manifest entry with name {qmdl_name}"),
|
||||
))?;
|
||||
if entry.qmdl_size_bytes == 0 {
|
||||
if entry.uncompressed_qmdl_size_bytes == 0 {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"QMDL file is empty, try again in a bit!".to_string(),
|
||||
));
|
||||
}
|
||||
let qmdl_size_bytes = entry.qmdl_size_bytes;
|
||||
let qmdl_file = qmdl_store
|
||||
let qmdl_reader = qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||
// the QMDL reader should stop at the last successfully written data chunk
|
||||
// (entry.size_bytes)
|
||||
let (reader, writer) = duplex(1024);
|
||||
|
||||
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_reader).await {
|
||||
error!("failed to generate PCAP: {e:?}");
|
||||
}
|
||||
});
|
||||
@@ -56,11 +68,7 @@ pub async fn get_pcap(
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
pub async fn generate_pcap_data<R, W>(
|
||||
writer: W,
|
||||
qmdl_file: R,
|
||||
qmdl_size_bytes: usize,
|
||||
) -> Result<(), Error>
|
||||
pub async fn generate_pcap_data<R, W>(writer: W, mut reader: QmdlReader<R>) -> Result<(), Error>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
R: AsyncRead + Unpin,
|
||||
@@ -68,7 +76,6 @@ where
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
|
||||
pcap_writer.write_iface_header().await?;
|
||||
|
||||
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||
while let Some(container) = reader.get_next_messages_container().await? {
|
||||
if container.data_type != DataType::UserSpace {
|
||||
continue;
|
||||
|
||||
+66
-14
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use log::{info, warn};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use rayhunter::util::RuntimeMetadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
@@ -45,15 +46,32 @@ pub struct Manifest {
|
||||
pub entries: Vec<ManifestEntry>,
|
||||
}
|
||||
|
||||
/// The structure of an entry in the QMDL manifest table
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct ManifestEntry {
|
||||
/// The name of the entry
|
||||
pub name: String,
|
||||
/// The system time when recording began
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub start_time: DateTime<Local>,
|
||||
/// The system time when the last message was recorded to the file
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub last_message_time: Option<DateTime<Local>>,
|
||||
pub qmdl_size_bytes: usize,
|
||||
/// The size of the uncompressed QMDL data in bytes. Previously this was
|
||||
/// called `qmdl_size_bytes`, so alias it for backwards compatibility.
|
||||
#[serde(alias = "qmdl_size_bytes")]
|
||||
pub uncompressed_qmdl_size_bytes: usize,
|
||||
/// The rayhunter daemon version which generated the file
|
||||
pub rayhunter_version: Option<String>,
|
||||
/// The OS which created the file
|
||||
pub system_os: Option<String>,
|
||||
/// The architecture on which the OS was running
|
||||
pub arch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stop_reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub compressed: bool,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
@@ -64,16 +82,22 @@ impl ManifestEntry {
|
||||
name: format!("{}", now.timestamp()),
|
||||
start_time: now,
|
||||
last_message_time: None,
|
||||
qmdl_size_bytes: 0,
|
||||
uncompressed_qmdl_size_bytes: 0,
|
||||
rayhunter_version: Some(metadata.rayhunter_version),
|
||||
system_os: Some(metadata.system_os),
|
||||
arch: Some(metadata.arch),
|
||||
stop_reason: None,
|
||||
compressed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_qmdl_filepath<P: AsRef<Path>>(&self, path: P) -> PathBuf {
|
||||
let mut filepath = path.as_ref().join(&self.name);
|
||||
filepath.set_extension("qmdl");
|
||||
if self.compressed {
|
||||
filepath.set_extension("qmdl.gz");
|
||||
} else {
|
||||
filepath.set_extension("qmdl");
|
||||
}
|
||||
filepath
|
||||
}
|
||||
|
||||
@@ -139,8 +163,9 @@ impl RecordingStore {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// QMDL files. We expect these files to be named like "<timestamp>.qmdl"
|
||||
// or "<timestamp>.qmdl.gz", and skip any files which don't match that
|
||||
// pattern.
|
||||
pub async fn recover<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
@@ -160,11 +185,14 @@ impl RecordingStore {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !filename.ends_with(".qmdl") {
|
||||
let (stem, compressed) = if filename.ends_with(".qmdl") {
|
||||
(filename.trim_end_matches(".qmdl"), false)
|
||||
} else if filename.ends_with(".qmdl.gz") {
|
||||
(filename.trim_end_matches(".qmdl.gz"), true)
|
||||
} else {
|
||||
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;
|
||||
@@ -191,12 +219,14 @@ impl RecordingStore {
|
||||
info!("successfully recovered QMDL entry {os_filename:?}!");
|
||||
manifest_entries.push(ManifestEntry {
|
||||
name: stem.to_string(),
|
||||
compressed,
|
||||
start_time: start_time.into(),
|
||||
last_message_time: Some(last_message_time.into()),
|
||||
qmdl_size_bytes: metadata.size() as usize,
|
||||
uncompressed_qmdl_size_bytes: metadata.size() as usize,
|
||||
rayhunter_version: None,
|
||||
system_os: None,
|
||||
arch: None,
|
||||
stop_reason: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,11 +280,19 @@ impl RecordingStore {
|
||||
}
|
||||
|
||||
// Returns the corresponding QMDL file for a given entry
|
||||
pub async fn open_entry_qmdl(&self, entry_index: usize) -> Result<File, RecordingStoreError> {
|
||||
pub async fn open_entry_qmdl(
|
||||
&self,
|
||||
entry_index: usize,
|
||||
) -> Result<QmdlReader<File>, RecordingStoreError> {
|
||||
let entry = &self.manifest.entries[entry_index];
|
||||
File::open(entry.get_qmdl_filepath(&self.path))
|
||||
let file = File::open(entry.get_qmdl_filepath(&self.path))
|
||||
.await
|
||||
.map_err(RecordingStoreError::ReadFileError)
|
||||
.map_err(RecordingStoreError::ReadFileError)?;
|
||||
Ok(QmdlReader::new(
|
||||
file,
|
||||
entry.compressed,
|
||||
Some(entry.uncompressed_qmdl_size_bytes),
|
||||
))
|
||||
}
|
||||
|
||||
// Returns the corresponding QMDL file for a given entry
|
||||
@@ -299,7 +337,7 @@ impl RecordingStore {
|
||||
entry_index: usize,
|
||||
size_bytes: usize,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
||||
self.manifest.entries[entry_index].uncompressed_qmdl_size_bytes = size_bytes;
|
||||
self.manifest.entries[entry_index].last_message_time =
|
||||
Some(rayhunter::clock::get_adjusted_now());
|
||||
self.write_manifest().await
|
||||
@@ -342,6 +380,17 @@ impl RecordingStore {
|
||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||
}
|
||||
|
||||
pub async fn set_current_stop_reason(
|
||||
&mut self,
|
||||
reason: String,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
if let Some(idx) = self.current_entry {
|
||||
self.manifest.entries[idx].stop_reason = Some(reason);
|
||||
self.write_manifest().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_current_entry(&self, name: &str) -> bool {
|
||||
match self.current_entry {
|
||||
Some(idx) => match self.manifest.entries.get(idx) {
|
||||
@@ -464,7 +513,10 @@ mod tests {
|
||||
.entry_for_name(&store.manifest.entries[entry_index].name)
|
||||
.unwrap();
|
||||
assert!(entry.last_message_time.is_some());
|
||||
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
|
||||
assert_eq!(
|
||||
store.manifest.entries[entry_index].uncompressed_qmdl_size_bytes,
|
||||
1000
|
||||
);
|
||||
assert_eq!(
|
||||
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||
store.manifest
|
||||
|
||||
+137
-33
@@ -14,16 +14,17 @@ use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::write;
|
||||
use tokio::io::{AsyncReadExt, copy, duplex};
|
||||
use tokio::io::copy;
|
||||
use tokio::io::duplex;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::DiagDeviceCtrlMessage;
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
use crate::config::Config;
|
||||
use crate::diag::DiagDeviceCtrlMessage;
|
||||
use crate::display::DisplayState;
|
||||
use crate::pcap::generate_pcap_data;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
@@ -39,6 +40,21 @@ pub struct ServerState {
|
||||
pub ui_update_sender: Option<Sender<DisplayState>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/qmdl/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "QMDL download successful", content_type = "application/octet-stream"),
|
||||
(status = StatusCode::NOT_FOUND, description = "Could not find file {name}"),
|
||||
(status = StatusCode::SERVICE_UNAVAILABLE, description = "QMDL file is empty, or error opening file")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL filename to convert and download")
|
||||
),
|
||||
summary = "Download a QMDL file",
|
||||
description = "Stream the QMDL file {name} to the client."
|
||||
))]
|
||||
pub async fn get_qmdl(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
@@ -49,7 +65,7 @@ pub async fn get_qmdl(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find qmdl file with name {qmdl_idx}"),
|
||||
))?;
|
||||
let qmdl_file = qmdl_store
|
||||
let qmdl_reader = qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -58,14 +74,15 @@ pub async fn get_qmdl(
|
||||
format!("error opening QMDL file: {err}"),
|
||||
)
|
||||
})?;
|
||||
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
||||
|
||||
let headers = [
|
||||
(CONTENT_TYPE, "application/octet-stream"),
|
||||
(CONTENT_LENGTH, &entry.qmdl_size_bytes.to_string()),
|
||||
(
|
||||
CONTENT_LENGTH,
|
||||
&entry.uncompressed_qmdl_size_bytes.to_string(),
|
||||
),
|
||||
];
|
||||
let body = Body::from_stream(qmdl_stream);
|
||||
let body = Body::from_stream(qmdl_reader.as_stream());
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
@@ -106,12 +123,38 @@ pub async fn serve_static(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/config",
|
||||
tag = "Configuration",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = Config)
|
||||
),
|
||||
summary = "Get config",
|
||||
description = "Show the running configuration for Rayhunter."
|
||||
))]
|
||||
pub async fn get_config(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<Config>, (StatusCode, String)> {
|
||||
Ok(Json(state.config.clone()))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/config",
|
||||
tag = "Configuration",
|
||||
request_body(
|
||||
content = Option<[Config]>,
|
||||
description = "Any or all configuration elements from the valid config schema to be altered may be passed. Invalid keys will be discarded. Invalid values or value types will return an error."
|
||||
),
|
||||
responses(
|
||||
(status = StatusCode::ACCEPTED, description = "Success"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Failed to parse or write config file"),
|
||||
(status = 422, description = "Failed to deserialize JSON body")
|
||||
),
|
||||
summary = "Set config",
|
||||
description = "Write a new configuration for Rayhunter and trigger a restart."
|
||||
))]
|
||||
pub async fn set_config(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Json(config): Json<Config>,
|
||||
@@ -138,6 +181,18 @@ pub async fn set_config(
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/test-notification",
|
||||
tag = "Configuration",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success"),
|
||||
(status = StatusCode::BAD_REQUEST, description = "No notification URL set"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Failed to send HTTP request. Ensure your device can reach the internet.")
|
||||
),
|
||||
summary = "Test ntfy notification",
|
||||
description = "Send a test notification to the ntfy_url in the running configuration for Rayhunter."
|
||||
))]
|
||||
pub async fn test_notification(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -174,10 +229,13 @@ pub async fn test_notification(
|
||||
|
||||
/// Response for GET /api/time
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct TimeResponse {
|
||||
/// The raw system time (without clock offset)
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub system_time: DateTime<Local>,
|
||||
/// The adjusted time (system time + offset)
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub adjusted_time: DateTime<Local>,
|
||||
/// The current offset in seconds
|
||||
pub offset_seconds: i64,
|
||||
@@ -185,11 +243,22 @@ pub struct TimeResponse {
|
||||
|
||||
/// Request for POST /api/time-offset
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct SetTimeOffsetRequest {
|
||||
/// The offset to set, in seconds
|
||||
pub offset_seconds: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/time",
|
||||
tag = "Configuration",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = TimeResponse)
|
||||
),
|
||||
summary = "Get time",
|
||||
description = "Get the current time and offset (in seconds) of the device."
|
||||
))]
|
||||
pub async fn get_time() -> Json<TimeResponse> {
|
||||
let system_time = Local::now();
|
||||
let adjusted_time = rayhunter::clock::get_adjusted_now();
|
||||
@@ -203,31 +272,59 @@ pub async fn get_time() -> Json<TimeResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/time-offset",
|
||||
tag = "Configuration",
|
||||
request_body(
|
||||
content = SetTimeOffsetRequest
|
||||
),
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = TimeResponse)
|
||||
),
|
||||
summary = "Set time offset",
|
||||
description = "Set the difference (in seconds) between the system time and the adjusted time for Rayhunter."
|
||||
))]
|
||||
pub async fn set_time_offset(Json(req): Json<SetTimeOffsetRequest>) -> StatusCode {
|
||||
rayhunter::clock::set_offset(chrono::TimeDelta::seconds(req.offset_seconds));
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/zip/{name}",
|
||||
tag = "Recordings",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "ZIP download successful. It is possible that if the PCAP fails to convert, the same status will be returned, but the file will contain only the QMDL file.", content_type = "application/zip"),
|
||||
(status = StatusCode::NOT_FOUND, description = "Could not find file {name}"),
|
||||
(status = StatusCode::SERVICE_UNAVAILABLE, description = "QMDL file is empty, or error opening file")
|
||||
),
|
||||
params(
|
||||
("name" = String, Path, description = "QMDL filename to convert and download")
|
||||
),
|
||||
summary = "Download a ZIP file",
|
||||
description = "Stream a ZIP file to the client which contains the QMDL file {name} and a PCAP generated from the same file."
|
||||
))]
|
||||
pub async fn get_zip(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(entry_name): Path<String>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_idx = entry_name.trim_end_matches(".zip").to_owned();
|
||||
let (entry_index, qmdl_size_bytes) = {
|
||||
let (entry_index, compressed) = {
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find entry with name {qmdl_idx}"),
|
||||
))?;
|
||||
|
||||
if entry.qmdl_size_bytes == 0 {
|
||||
if entry.uncompressed_qmdl_size_bytes == 0 {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"QMDL file is empty, try again in a bit!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
(entry_index, entry.qmdl_size_bytes)
|
||||
(entry_index, entry.compressed)
|
||||
};
|
||||
|
||||
let qmdl_store_lock = state.qmdl_store_lock.clone();
|
||||
@@ -240,22 +337,18 @@ pub async fn get_zip(
|
||||
|
||||
// Add QMDL file
|
||||
{
|
||||
let entry =
|
||||
ZipEntryBuilder::new(format!("{qmdl_idx}.qmdl").into(), Compression::Stored);
|
||||
let extension = if compressed { "qmdl.gz" } else { "qmdl" };
|
||||
let entry = ZipEntryBuilder::new(
|
||||
format!("{qmdl_idx}.{extension}").into(),
|
||||
Compression::Stored,
|
||||
);
|
||||
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
|
||||
// not impl tokio's AsyncWrite, but only future's AsyncWrite. This can be removed
|
||||
// once https://github.com/Majored/rs-async-zip/pull/160 is released.
|
||||
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
|
||||
|
||||
let mut qmdl_file = {
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await?
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
|
||||
copy(&mut qmdl_file, &mut entry_writer).await?;
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
let mut qmdl_reader = qmdl_store.open_entry_qmdl(entry_index).await?;
|
||||
copy(&mut qmdl_reader, &mut entry_writer).await?;
|
||||
entry_writer.into_inner().close().await?;
|
||||
}
|
||||
|
||||
@@ -265,17 +358,10 @@ pub async fn get_zip(
|
||||
ZipEntryBuilder::new(format!("{qmdl_idx}.pcapng").into(), Compression::Stored);
|
||||
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
|
||||
|
||||
let qmdl_file_for_pcap = {
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await?
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
let qmdl_reader = qmdl_store.open_entry_qmdl(entry_index).await?;
|
||||
|
||||
if let Err(e) =
|
||||
generate_pcap_data(&mut entry_writer, qmdl_file_for_pcap, qmdl_size_bytes).await
|
||||
{
|
||||
if let Err(e) = generate_pcap_data(&mut entry_writer, qmdl_reader).await {
|
||||
// if we fail to generate the PCAP file, we should still continue and give the
|
||||
// user the QMDL.
|
||||
error!("Failed to generate PCAP: {e:?}");
|
||||
@@ -299,6 +385,21 @@ pub async fn get_zip(
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/debug/display-state",
|
||||
tag = "Configuration",
|
||||
request_body(
|
||||
content = DisplayState
|
||||
),
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Display state updated successfully"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error sending update to the display"),
|
||||
(status = StatusCode::SERVICE_UNAVAILABLE, description = "Display system not available")
|
||||
),
|
||||
summary = "Set display state",
|
||||
description = "Change the display state (color bar or otherwise) of the device for debugging purposes."
|
||||
))]
|
||||
pub async fn debug_set_display_state(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Json(display_state): Json<DisplayState>,
|
||||
@@ -420,7 +521,10 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
filenames,
|
||||
vec![format!("{entry_name}.qmdl"), format!("{entry_name}.pcapng"),]
|
||||
vec![
|
||||
format!("{entry_name}.qmdl.gz"),
|
||||
format!("{entry_name}.pcapng"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+83
-22
@@ -1,3 +1,4 @@
|
||||
use std::ffi::CString;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::battery::get_battery_status;
|
||||
@@ -13,7 +14,9 @@ use rayhunter::{Device, util::RuntimeMetadata};
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Structure of device system statistics
|
||||
#[derive(Debug, Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct SystemStats {
|
||||
pub disk_stats: DiskStats,
|
||||
pub memory_stats: MemoryStats,
|
||||
@@ -25,7 +28,7 @@ pub struct SystemStats {
|
||||
impl SystemStats {
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
disk_stats: DiskStats::new(qmdl_path, device).await?,
|
||||
disk_stats: DiskStats::new(qmdl_path)?,
|
||||
memory_stats: MemoryStats::new(device).await?,
|
||||
runtime_metadata: RuntimeMetadata::new(),
|
||||
battery_status: match get_battery_status(device).await {
|
||||
@@ -40,49 +43,71 @@ impl SystemStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Device storage information
|
||||
#[derive(Debug, Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct DiskStats {
|
||||
/// The partition to which the daemon is installed
|
||||
partition: String,
|
||||
/// The total disk size of the partition
|
||||
total_size: String,
|
||||
/// Total used size of the partition
|
||||
used_size: String,
|
||||
/// Remaining free space of the partition
|
||||
available_size: String,
|
||||
/// Disk usage displayed as percentage
|
||||
used_percent: String,
|
||||
/// The root folder to which the partition is mounted
|
||||
mounted_on: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub available_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
impl DiskStats {
|
||||
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
|
||||
// the QMDL file.
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
// Uz801 needs to be told to use the busybox df specifically
|
||||
let mut df_cmd: Command;
|
||||
if matches!(device, Device::Uz801) {
|
||||
df_cmd = Command::new("busybox");
|
||||
df_cmd.arg("df");
|
||||
} else {
|
||||
df_cmd = Command::new("df");
|
||||
#[allow(clippy::unnecessary_cast)] // c_ulong is u32 on ARM, u64 on macOS
|
||||
pub fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
let c_path =
|
||||
CString::new(qmdl_path).map_err(|e| format!("invalid path {qmdl_path}: {e}"))?;
|
||||
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 {
|
||||
return Err(format!(
|
||||
"statvfs({qmdl_path}) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
df_cmd.arg("-h");
|
||||
df_cmd.arg(qmdl_path);
|
||||
let stdout = get_cmd_output(df_cmd).await?;
|
||||
|
||||
// Handle standard df -h format
|
||||
let mut parts = stdout.split_whitespace().skip(7);
|
||||
let block_size = stat.f_frsize as u64;
|
||||
let total_kb = (stat.f_blocks as u64 * block_size / 1024) as usize;
|
||||
let free_kb = (stat.f_bfree as u64 * block_size / 1024) as usize;
|
||||
let available_kb = (stat.f_bavail as u64 * block_size / 1024) as usize;
|
||||
let used_kb = total_kb.saturating_sub(free_kb);
|
||||
let used_percent = if stat.f_blocks > 0 {
|
||||
format!("{}%", (stat.f_blocks - stat.f_bfree) * 100 / stat.f_blocks)
|
||||
} else {
|
||||
"0%".to_string()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
partition: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
partition: qmdl_path.to_string(),
|
||||
total_size: humanize_kb(total_kb),
|
||||
used_size: humanize_kb(used_kb),
|
||||
available_size: humanize_kb(available_kb),
|
||||
used_percent,
|
||||
mounted_on: qmdl_path.to_string(),
|
||||
available_bytes: Some(stat.f_bavail as u64 * block_size),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Device memory information
|
||||
#[derive(Debug, Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct MemoryStats {
|
||||
/// The total memory available on the device
|
||||
total: String,
|
||||
/// The currently used memory
|
||||
used: String,
|
||||
/// Remaining free memory
|
||||
free: String,
|
||||
}
|
||||
|
||||
@@ -135,6 +160,17 @@ fn humanize_kb(kb: usize) -> String {
|
||||
format!("{:.1}M", kb as f64 / 1024.0)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/system-stats",
|
||||
tag = "Statistics",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = SystemStats),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error collecting statistics")
|
||||
),
|
||||
summary = "Get system info",
|
||||
description = "Display system/device statistics."
|
||||
))]
|
||||
pub async fn get_system_stats(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<SystemStats>, (StatusCode, String)> {
|
||||
@@ -151,12 +187,26 @@ pub async fn get_system_stats(
|
||||
}
|
||||
}
|
||||
|
||||
/// QMDL manifest information
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct ManifestStats {
|
||||
/// A vector containing the names of the QMDL files
|
||||
pub entries: Vec<ManifestEntry>,
|
||||
/// The currently open QMDL file
|
||||
pub current_entry: Option<ManifestEntry>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/qmdl-manifest",
|
||||
tag = "Statistics",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = ManifestStats)
|
||||
),
|
||||
summary = "QMDL Manifest",
|
||||
description = "List QMDL files available on the device and some of their basic statistics."
|
||||
))]
|
||||
pub async fn get_qmdl_manifest(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<ManifestStats>, (StatusCode, String)> {
|
||||
@@ -169,6 +219,17 @@ pub async fn get_qmdl_manifest(
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/log",
|
||||
tag = "Statistics",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", content_type = "text/plain"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Could not read /data/rayhunter/rayhunter.log file")
|
||||
),
|
||||
summary = "Display log",
|
||||
description = "Download the current device log in UTF-8 plaintext."
|
||||
))]
|
||||
pub async fn get_log() -> Result<String, (StatusCode, String)> {
|
||||
tokio::fs::read_to_string("/data/rayhunter/rayhunter.log")
|
||||
.await
|
||||
|
||||
Generated
+197
-158
@@ -10,7 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
@@ -21,7 +21,7 @@
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.0.0",
|
||||
@@ -819,9 +819,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
|
||||
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -833,9 +833,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -847,9 +847,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -861,9 +861,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -875,9 +875,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -889,9 +889,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -903,9 +903,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -917,9 +917,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -931,9 +931,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -945,9 +945,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -959,9 +959,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -973,9 +987,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -987,9 +1015,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1001,9 +1029,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1015,9 +1043,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1029,9 +1057,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1043,9 +1071,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1056,10 +1084,24 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1071,9 +1113,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1085,9 +1127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1099,9 +1141,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1113,9 +1155,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1167,25 +1209,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.49.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.5.tgz",
|
||||
"integrity": "sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==",
|
||||
"version": "2.53.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz",
|
||||
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1196,10 +1236,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
@@ -1216,7 +1256,6 @@
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
@@ -1305,11 +1344,17 @@
|
||||
"integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
|
||||
@@ -1356,7 +1401,6 @@
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
@@ -1512,13 +1556,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -1690,7 +1734,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1776,9 +1819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1803,9 +1846,9 @@
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -1944,7 +1987,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -2201,9 +2243,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2320,7 +2362,6 @@
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2509,9 +2550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
||||
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2708,9 +2749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -2815,13 +2856,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -3032,7 +3073,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -3215,9 +3255,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3228,9 +3268,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3501,12 +3541,11 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3554,7 +3593,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3755,7 +3793,6 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -3875,9 +3912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3891,28 +3928,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.4",
|
||||
"@rollup/rollup-android-arm64": "4.52.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.4",
|
||||
"@rollup/rollup-darwin-x64": "4.52.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.4",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.4",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.4",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.4",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -3967,9 +4007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4235,23 +4275,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.39.10",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.10.tgz",
|
||||
"integrity": "sha512-Q3gqCGIgl4r0CR7OaWYjVo22nqFmLLSfn1MiWNFaITamvqhGBD3kyqk51EKuO4Nd1z8QliO5KIz7a2Ka9Rxilw==",
|
||||
"version": "5.53.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
|
||||
"integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.1.0",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -4451,9 +4492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4665,7 +4706,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4759,7 +4799,6 @@
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.0.0",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
dateStyle: 'short',
|
||||
});
|
||||
|
||||
const analyzers = report.metadata.analyzers;
|
||||
const analyzers = $derived(report.metadata.analyzers);
|
||||
|
||||
const skipped_messages: Map<string, number> = $derived.by(() => {
|
||||
let map = new Map();
|
||||
|
||||
@@ -22,10 +22,26 @@
|
||||
<p>Error getting analysis report: {entry.analysis_report}</p>
|
||||
{:else}
|
||||
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
||||
{@const numWarnings: number = entry.get_num_warnings() || 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if !current}
|
||||
<div class="flex flex-row justify-end items-center">
|
||||
<ReAnalyzeButton {entry} {manager} />
|
||||
{#if !!numWarnings || !current}
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
{#if !!numWarnings}
|
||||
<div
|
||||
class="text-red-700 border-red-500 border rounded-lg text-blue-600 px-2 py-1 mr-12"
|
||||
>
|
||||
Your Rayhunter device raised {`${numWarnings}`} warning{`${
|
||||
numWarnings > 1 ? 's' : ''
|
||||
}`}!
|
||||
<a
|
||||
href="https://efforg.github.io/rayhunter/faq.html#red"
|
||||
class="text-blue-600 underline">Read the FAQ</a
|
||||
> to learn what you can do about it
|
||||
</div>
|
||||
{/if}
|
||||
{#if !current}
|
||||
<ReAnalyzeButton {entry} {manager} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.analysis_report.rows.length > 0}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { get_config, set_config, test_notification, type Config } from '../utils.svelte';
|
||||
import Modal from './Modal.svelte';
|
||||
|
||||
let { shown = $bindable() }: { shown: boolean } = $props();
|
||||
let config = $state<Config | null>(null);
|
||||
|
||||
let loading = $state(false);
|
||||
@@ -10,7 +12,6 @@
|
||||
let messageType = $state<'success' | 'error' | null>(null);
|
||||
let testMessage = $state('');
|
||||
let testMessageType = $state<'success' | 'error' | null>(null);
|
||||
let showConfig = $state(false);
|
||||
|
||||
async function load_config() {
|
||||
try {
|
||||
@@ -60,30 +61,14 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showConfig && !config) {
|
||||
if (shown && !config) {
|
||||
load_config();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 m-4">
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
<span>Configuration</span>
|
||||
<svg
|
||||
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>
|
||||
</button>
|
||||
|
||||
{#if showConfig}
|
||||
<Modal bind:shown title="Configuration">
|
||||
<div class="p-2">
|
||||
{#if loading}
|
||||
<div class="text-center py-4">Loading config...</div>
|
||||
{:else if config}
|
||||
@@ -128,8 +113,7 @@
|
||||
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={1}
|
||||
>1 - Double-tap power button to start/stop recording</option
|
||||
<option value={1}>1 - Double-tap power button to start new recording</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
@@ -241,6 +225,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Storage Management</h3>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="min_space_to_start_recording_mb"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Minimum Space to Start Recording (MB)
|
||||
</label>
|
||||
<input
|
||||
id="min_space_to_start_recording_mb"
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={config.min_space_to_start_recording_mb}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Recording will not start if less than this amount of disk space is free
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="min_space_to_continue_recording_mb"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Minimum Space to Continue Recording (MB)
|
||||
</label>
|
||||
<input
|
||||
id="min_space_to_continue_recording_mb"
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={config.min_space_to_continue_recording_mb}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Recording will stop automatically if disk space drops below this level
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
Analyzer Heuristic Settings
|
||||
@@ -396,5 +422,5 @@
|
||||
Failed to load configuration. Please try reloading the page.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,34 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { get_logs } from '$lib/utils.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Modal from './Modal.svelte';
|
||||
|
||||
let { shown = $bindable() }: { shown: boolean } = $props();
|
||||
let content: string | undefined = $state(undefined);
|
||||
|
||||
onMount(() => {
|
||||
// Used by LogView modal
|
||||
window.addEventListener('scroll', () => {
|
||||
document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`);
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (shown) {
|
||||
const scrollY = document.documentElement.style.getPropertyValue('--scroll-y');
|
||||
const body = document.body;
|
||||
body.style.position = 'fixed';
|
||||
body.style.top = `-${scrollY}`;
|
||||
} else {
|
||||
const body = document.body;
|
||||
const scrollY = body.style.top;
|
||||
body.style.position = '';
|
||||
body.style.top = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
// Don't update UI if browser tab isn't visible
|
||||
if (content !== undefined && (document.hidden || !shown)) {
|
||||
return;
|
||||
}
|
||||
@@ -42,33 +21,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if shown}
|
||||
<div
|
||||
class="fixed left-5 right-5 top-5 bottom-5 z-50 bg-white border border-white rounded-md
|
||||
flex flex-col p-2 drop-shadow"
|
||||
>
|
||||
<div class="flex h-20 justify-between items-center p-1">
|
||||
<span class="text-2xl mb-2">Log</span>
|
||||
<button onclick={() => (shown = false)} aria-label="close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z"
|
||||
fill="#0F1729"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-100 border border-gray-100 rounded-md overflow-scroll">
|
||||
<pre class="m-2">{content}</pre>
|
||||
</div>
|
||||
<Modal bind:shown title="Logs">
|
||||
<div class="bg-gray-100 border border-gray-100 rounded-md overflow-scroll">
|
||||
<pre class="m-2">{content}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
@@ -81,6 +81,11 @@
|
||||
'N/A'}</span
|
||||
>
|
||||
</div>
|
||||
{#if entry.stop_reason}
|
||||
<div class="bg-yellow-50 border border-yellow-300 rounded p-2 text-yellow-800 text-sm">
|
||||
{entry.stop_reason}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
shown = $bindable(),
|
||||
title,
|
||||
children,
|
||||
}: { shown: boolean; title: string; children: Snippet } = $props();
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`);
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (shown) {
|
||||
const scrollY = document.documentElement.style.getPropertyValue('--scroll-y');
|
||||
const body = document.body;
|
||||
body.style.position = 'fixed';
|
||||
body.style.top = `-${scrollY}`;
|
||||
} else {
|
||||
const body = document.body;
|
||||
const scrollY = body.style.top;
|
||||
body.style.position = '';
|
||||
body.style.top = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if shown}
|
||||
<div
|
||||
class="fixed left-5 right-5 top-5 bottom-5 z-50 bg-white border border-white rounded-md
|
||||
flex flex-col p-2 drop-shadow"
|
||||
>
|
||||
<div class="flex justify-between items-center p-1">
|
||||
<span class="text-2xl">{title}</span>
|
||||
<button onclick={() => (shown = false)} aria-label="close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z"
|
||||
fill="#0F1729"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -11,6 +11,7 @@ interface JsonManifestEntry {
|
||||
start_time: string;
|
||||
last_message_time: string;
|
||||
qmdl_size_bytes: number;
|
||||
stop_reason: string | null;
|
||||
}
|
||||
|
||||
export class Manifest {
|
||||
@@ -57,6 +58,7 @@ export class ManifestEntry {
|
||||
public analysis_size_bytes = $state(0);
|
||||
public analysis_status: AnalysisStatus | undefined = $state(undefined);
|
||||
public analysis_report: AnalysisReport | string | undefined = $state(undefined);
|
||||
public stop_reason: string | undefined = $state(undefined);
|
||||
|
||||
constructor(json: JsonManifestEntry) {
|
||||
this.name = json.name;
|
||||
@@ -65,6 +67,9 @@ export class ManifestEntry {
|
||||
if (json.last_message_time) {
|
||||
this.last_message_time = new Date(json.last_message_time);
|
||||
}
|
||||
if (json.stop_reason) {
|
||||
this.stop_reason = json.stop_reason;
|
||||
}
|
||||
}
|
||||
|
||||
get_readable_qmdl_size(): string {
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface DiskStats {
|
||||
available_size: string;
|
||||
used_percent: string;
|
||||
mounted_on: string;
|
||||
available_bytes?: number;
|
||||
}
|
||||
|
||||
export interface MemoryStats {
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface Config {
|
||||
ntfy_url: string;
|
||||
enabled_notifications: enabled_notifications[];
|
||||
analyzers: AnalyzerConfig;
|
||||
min_space_to_start_recording_mb: number;
|
||||
min_space_to_continue_recording_mb: number;
|
||||
}
|
||||
|
||||
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
let system_stats: SystemStats | undefined = $state(undefined);
|
||||
let update_error: string | undefined = $state(undefined);
|
||||
let logview_shown: boolean = $state(false);
|
||||
let config_shown: boolean = $state(false);
|
||||
$effect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
@@ -55,6 +56,7 @@
|
||||
</script>
|
||||
|
||||
<LogView bind:shown={logview_shown} />
|
||||
<ConfigForm bind:shown={config_shown} />
|
||||
<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" />
|
||||
@@ -103,6 +105,33 @@
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick={() => (config_shown = true)} class="flex flex-row gap-1 group">
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Config</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="M21 13v-2a1 1 0 0 0-1-1h-.757l-.707-1.707.535-.536a1 1 0 0 0 0-1.414l-1.414-1.414a1 1 0 0 0-1.414 0l-.536.535L14 5.757V5a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v.757L8.293 6.464l-.536-.535a1 1 0 0 0-1.414 0L4.929 7.343a1 1 0 0 0 0 1.414l.535.536L4.757 11H4a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h.757l.707 1.707-.535.536a1 1 0 0 0 0 1.414l1.414 1.414a1 1 0 0 0 1.414 0l.536-.535L10 18.243V19a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-.757l1.707-.707.536.535a1 1 0 0 0 1.414 0l1.414-1.414a1 1 0 0 0 0-1.414l-.535-.536.707-1.707H20a1 1 0 0 0 1-1Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px bg-white/30 self-stretch"></div>
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
@@ -197,7 +226,7 @@
|
||||
</span>
|
||||
<span
|
||||
>This webpage is not currently receiving updates from your Rayhunter device. This
|
||||
could be do loss of connection or some issue with your device.</span
|
||||
could be due to loss of connection or some issue with your device.</span
|
||||
>
|
||||
{#if update_error}
|
||||
<details>
|
||||
@@ -273,7 +302,6 @@
|
||||
<ManifestTable {entries} server_is_recording={!!current_entry} {manager} />
|
||||
</div>
|
||||
<DeleteAllButton />
|
||||
<ConfigForm />
|
||||
{:else}
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<!-- https://www.w3.org/WAI/tutorials/images/decorative/ -->
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: process.env.API_TARGET || 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: (proxy, _options) => {
|
||||
|
||||
Vendored
+7
-1
@@ -20,7 +20,7 @@ colorblind_mode = false
|
||||
ui_level = 1
|
||||
|
||||
# 0 = rayhunter does not read button presses
|
||||
# 1 = double-tapping the power button starts/stops recordings
|
||||
# 1 = double-tapping the power button starts new recording
|
||||
key_input_mode = 0
|
||||
|
||||
# If set, attempts to send a notification to the url when a new warning is triggered
|
||||
@@ -28,6 +28,12 @@ ntfy_url = ""
|
||||
# What notification types to enable. Does nothing if the above ntfy_url is not set.
|
||||
enabled_notifications = ["Warning", "LowBattery"]
|
||||
|
||||
# Disk Space Management
|
||||
# Minimum free space (MB) required to start recording
|
||||
min_space_to_start_recording_mb = 1
|
||||
# Minimum free space (MB) to continue recording (stops if below this)
|
||||
min_space_to_continue_recording_mb = 1
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](./introduction.md)
|
||||
- [Introduction](./introduction.md)
|
||||
- [Support, feedback, and community](./support-feedback-community.md)
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
- [Installation](./installation.md)
|
||||
@@ -22,3 +22,4 @@
|
||||
- [Wingtech CT2MHS01](./wingtech-ct2mhs01.md)
|
||||
- [PinePhone and PinePhone Pro](./pinephone.md)
|
||||
- [Moxee Hotspot](./moxee.md)
|
||||
- [REST API Documentation](./api-docs.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# REST API Documentation
|
||||
|
||||
The rayhunter daemon has [REST API documentation](./api-docs/) available in the interactive swagger-ui.
|
||||
|
||||
>**Note:** API endpoints are subject to change as needs arise, though we will try to keep them as stable as possible and notify about breaking changes in the changelogs for new versions.
|
||||
@@ -13,7 +13,7 @@ Through web UI you can set:
|
||||
- *High visibility (full screen color)*: fills the entire screen with the status color (green for recording, red for warnings, white for paused).
|
||||
- **Device Input Mode**, which defines behavior of built-in power button of the device. *Device Input Mode* could be:
|
||||
- *Disable button control*: built-in power button of the device is not used by Rayhunter.
|
||||
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
||||
- *Double-tap power button to start new recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
||||
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
|
||||
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
|
||||
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.warning-box {
|
||||
padding: 0.75em 1em;
|
||||
border-left: 4px solid #e33;
|
||||
border-radius: 4px;
|
||||
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
@@ -1,63 +1,94 @@
|
||||
# Installing from source
|
||||
|
||||
Building Rayhunter from source, either for development or because the install script doesn't work on your system, involves a number of external dependencies. Unless you need to do this, we recommend you use our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||
Building Rayhunter from source, either for development or otherwise, involves a
|
||||
number of external dependencies. Unless you need to do this, we recommend you
|
||||
use our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||
|
||||
* Install [nodejs/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), which is required to build Rayhunter's web UI
|
||||
* Make sure to build the site with `pushd daemon/web && npm install && npm run build && popd` before building Rayhunter. If you're working directly on the frontend, `npm run dev` will allow you to test a local frontend with hot-reloading (use `http://localhost:5173` instead of `http://localhost:8080`).
|
||||
* Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
|
||||
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
|
||||
* [linux](https://askubuntu.com/questions/652936/adding-android-sdk-platform-tools-to-path-downloaded-from-umake)
|
||||
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
|
||||
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
|
||||
* Install `curl` on your computer to run the install scripts. It is not needed to build binaries.
|
||||
At a high level, we have:
|
||||
|
||||
### Install Rust targets
|
||||
* A JS frontend written in SvelteKit (`./daemon/web/`)
|
||||
* A Rust binary `rayhunter-daemon` (`./daemon/`) that runs on the device, and bundles the frontend.
|
||||
* A Rust binary `installer` (`./installer`) that runs on the computer and bundles `rayhunter-daemon`.
|
||||
|
||||
[Install Rust the usual way](https://www.rust-lang.org/tools/install). Then,
|
||||
It's recommended to work either on Mac/Linux, or WSL on Windows.
|
||||
|
||||
- install the cross-compilation target for the device Rayhunter will run on:
|
||||
```sh
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
```
|
||||
## Building frontend and backend
|
||||
|
||||
- install the statically compiled target for your host machine to build the binary installer `serial`.
|
||||
```sh
|
||||
# check which toolchain you have installed by default with
|
||||
rustup show
|
||||
# now install the correct variant for your host platform, one of:
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
rustup target add armv7-unknown-linux-musleabi
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
```
|
||||
First, install dependencies:
|
||||
|
||||
Now you can root your device and install Rayhunter by running:
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [Node.js/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- C compiler tools (`apt install build-essential` on Linux, `xcode-select --install` on Mac)
|
||||
|
||||
Then you can build everything with:
|
||||
|
||||
```sh
|
||||
# Build the daemon binary for local development (rustcrypto TLS backend, fast compilation)
|
||||
# WARNING: The rustcrypto library, though not known to be insecure, is less well
|
||||
# tested than its counterpart and could potentially have severe issues in
|
||||
# its cryptographic implementation. We therefore recommend using ring-tls in
|
||||
# production builds (see below)
|
||||
cargo build-daemon-firmware-devel
|
||||
|
||||
# To build it exactly like in CI (more mature ring TLS backend, slower compilation)
|
||||
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
|
||||
|
||||
# Build rootshell
|
||||
cargo build-rootshell-firmware-devel
|
||||
|
||||
# Replace 'orbic' with your device type if different.
|
||||
# A list of possible values can be found with 'cargo run --bin installer help'.
|
||||
FIRMWARE_PROFILE=firmware-devel cargo run -p installer --bin installer orbic
|
||||
./scripts/build-dev.sh
|
||||
./scripts/install-dev.sh orbic # replace 'orbic' with your device type
|
||||
```
|
||||
|
||||
### If you're on Windows or can't run the install scripts
|
||||
## Hot-reloading the frontend
|
||||
|
||||
* Root your device on Windows using the instructions here: <https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183>
|
||||
* Build the web UI using `cd daemon/web && npm install && npm run build`
|
||||
* Push the scripts in `scripts/` to `/etc/init.d` on device and make a directory called `/data/rayhunter` using `adb shell` (and sshell for your root shell if you followed the steps above)
|
||||
* You also need to copy `config.toml.in` to `/data/rayhunter/config.toml`. Uncomment the `device` line and set the value to your device type if necessary.
|
||||
* Then run `./make.sh`, which will build the binary, push it over adb, and restart the device. Once it's restarted, Rayhunter should be running!
|
||||
If you are working on the frontend, you normally have to repeat all of the above steps everytime to see a change.
|
||||
|
||||
You can instead run the frontend separately on your PC while the Rust parts
|
||||
continue running on your target device:
|
||||
|
||||
```sh
|
||||
cd daemon/web
|
||||
|
||||
# Assumes rayhunter-daemon is listening on localhost:8080
|
||||
npm run dev
|
||||
|
||||
# Use a custom target IP:port where the backend runs
|
||||
API_TARGET=http://192.168.1.1:8080 npm run dev
|
||||
```
|
||||
|
||||
The UI will listen on `localhost:5173` and instantly show any frontend changes
|
||||
you make. Backend changes require building everything from the top (daemon and installer).
|
||||
|
||||
## Installer utils, getting a shell
|
||||
|
||||
Check `./scripts/install-dev.sh util --help`
|
||||
for useful utilities for transferring files, opening shells. The exact tools
|
||||
available wildly depend on the device you're working on, and they are
|
||||
usually documented the relevant device's page under [Supported
|
||||
Devices](./supported-devices.md).
|
||||
|
||||
A lot of devices run a trimmed down version of Android and have ADB (Android
|
||||
Debug Bridge) support. The USB-based installers (`orbic-usb`, `pinephone`,
|
||||
`uz801`) use ADB to perform the installation.
|
||||
|
||||
You might want to install and use actual ADB to connect to the device, push
|
||||
files and generally poke around. The installer contains some tools to enable ADB:
|
||||
|
||||
```sh
|
||||
adb kill-server
|
||||
|
||||
# Enables ADB on either of these devices
|
||||
./scripts/install-dev.sh util tmobile-start-adb
|
||||
./scripts/install-dev.sh orbic-usb
|
||||
|
||||
adb shell
|
||||
```
|
||||
|
||||
Note though that we can't assist with any issues setting ADB up, _especially
|
||||
not_ on Windows. There have been too many driver issues to make this the
|
||||
"golden path" for most users or contributors. There have been instances where
|
||||
people managed to brick their orbic devices using ADB on Windows.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
You may need to turn off your VPN in order to load the frontend succesfully - even with local network sharing enabled, VPNs can interfere with the connection to the backend.
|
||||
|
||||
Specifically for WSL users:
|
||||
|
||||
- The HyperV firewall also tends to interfere with the connection between frontend and backend. You can turn it off in your WSL settings.
|
||||
|
||||
- WSL2 has a known compatibility issue which may prevent vite from detecting file system changes and therefore affects HMR (hot module replacement).
|
||||
If your hot reloading does not work, some have success using polling to detect changes. To do so, specify the following setting in vite.config.ts:
|
||||
```ts
|
||||
server: {
|
||||
watch: { usePolling: true }
|
||||
}
|
||||
```
|
||||
+6
-1
@@ -5,6 +5,8 @@ Supported in Rayhunter since version 0.6.0.
|
||||
The Moxee Hotspot is a device very similar to the Orbic RC400L. It seems to be
|
||||
primarily for the US market.
|
||||
|
||||
**These devices have relatively little storage. The Orbic is usually a better alternative, though might be more expensive.**
|
||||
|
||||
- [KonnectONE product page](https://www.konnectone.com/specs-hotspot)
|
||||
- [Moxee product page](https://www.moxee.com/hotspot)
|
||||
|
||||
@@ -30,11 +32,14 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
|
||||
Connect to the hotspot's network using WiFi or USB tethering and run:
|
||||
|
||||
```sh
|
||||
./installer orbic --admin-password 'mypassword'
|
||||
./installer moxee --admin-password 'mypassword'
|
||||
```
|
||||
|
||||
The password (in place of `mypassword`) is under the battery.
|
||||
|
||||
`./installer moxee` is almost the same as `./installer orbic`, it just comes
|
||||
with slightly better defaults that will give you more space for recordings.
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="SwaggerUI" />
|
||||
<title>SwaggerUI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.31.0/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.31.0/swagger-ui-bundle.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.31.0/swagger-ui-standalone-preset.js" crossorigin></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: './rayhunter-openapi.json',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -26,6 +26,8 @@ You can access this UI in one of two ways:
|
||||
|
||||
* **Connect over USB (TP-Link):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
|
||||
|
||||
> **_NOTE:_** When downloading recordings, "Insecure download blocked" warnings can safely be ignored - this is due to Rayhunter not using HTTPS.
|
||||
|
||||
## Key shortcuts
|
||||
|
||||
As of Rayhunter version 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well. This feature is disabled by default since Rayhunter version 0.4.0 and needs to be enabled through [configuration](./configuration.md).
|
||||
|
||||
Generated
+184
-134
@@ -16,7 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/kit": "^2.53.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"eslint": "^9.38.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
@@ -774,9 +774,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -787,9 +787,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -800,9 +800,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -813,9 +813,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -826,9 +826,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -839,9 +839,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -852,9 +852,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -865,9 +865,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -878,9 +878,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -891,9 +891,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -904,9 +904,22 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -917,9 +930,22 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -930,9 +956,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -943,9 +969,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -956,9 +982,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -969,9 +995,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -982,9 +1008,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -994,10 +1020,23 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1008,9 +1047,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1021,9 +1060,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1034,9 +1073,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1047,9 +1086,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1087,9 +1126,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.50.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.1.tgz",
|
||||
"integrity": "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.0.tgz",
|
||||
"integrity": "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1098,13 +1137,12 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1115,10 +1153,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
@@ -1736,6 +1774,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
@@ -1937,13 +1982,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -2058,9 +2103,9 @@
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2271,9 +2316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2523,9 +2568,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz",
|
||||
"integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2701,9 +2746,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -3264,9 +3309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3277,9 +3322,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3431,9 +3476,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -3682,9 +3727,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -3697,28 +3742,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
||||
"@rollup/rollup-android-arm64": "4.52.5",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.5",
|
||||
"@rollup/rollup-darwin-x64": "4.52.5",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.5",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.5",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.5",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.5",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.5",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.5",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.5",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -3773,9 +3821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3853,9 +3901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.43.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.2.tgz",
|
||||
"integrity": "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==",
|
||||
"version": "5.53.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz",
|
||||
"integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3863,12 +3911,14 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.1.0",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/kit": "^2.53.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"eslint": "^9.38.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"globals": "^16.4.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer-gui"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -21,4 +21,5 @@ tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1.0.100"
|
||||
shlex = "1"
|
||||
installer = { path = "../../installer" }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use anyhow::Context;
|
||||
use tauri::Emitter;
|
||||
|
||||
async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> {
|
||||
let args_vec = shlex::split(&args).context("Failed to parse arguments: unclosed quote")?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
installer::run_with_callback(
|
||||
// TODO: we should split using something similar to shlex in python
|
||||
args.split_whitespace(),
|
||||
args_vec.iter().map(|s| s.as_str()),
|
||||
Some(Box::new(move |output| {
|
||||
app_handle
|
||||
.emit("installer-output", output)
|
||||
|
||||
@@ -81,6 +81,9 @@
|
||||
<input
|
||||
class="mr-1 px-5 py-2 rounded-lg shadow-md"
|
||||
placeholder="Enter CLI installer args..."
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
bind:value={installerArgs}
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
@@ -28,7 +28,7 @@ nusb = "0.1.13"
|
||||
reqwest = { version = "0.12.15", features = ["json"], default-features = false }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false }
|
||||
tokio = { version = "1.44.2", features = ["io-util", "io-std", "macros", "rt"], default-features = false }
|
||||
tokio-retry2 = "0.5.7"
|
||||
tokio-stream = "0.1.17"
|
||||
futures = "0.3"
|
||||
|
||||
+8
-1
@@ -4,7 +4,14 @@ use std::process::exit;
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| "firmware".to_string());
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| {
|
||||
// Default to firmware-devel for debug builds, firmware for release builds
|
||||
if std::env::var("PROFILE").as_deref() == Ok("release") {
|
||||
"firmware".to_string()
|
||||
} else {
|
||||
"firmware-devel".to_string()
|
||||
}
|
||||
});
|
||||
let include_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../target/armv7-unknown-linux-musleabihf")
|
||||
.join(&profile);
|
||||
|
||||
+118
-5
@@ -1,9 +1,9 @@
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use crate::output::println;
|
||||
use crate::output::{print, println};
|
||||
|
||||
/// Abstraction for device communication (telnet or ADB)
|
||||
pub trait DeviceConnection {
|
||||
@@ -17,19 +17,20 @@ pub trait DeviceConnection {
|
||||
|
||||
/// Check if a file exists using a DeviceConnection
|
||||
pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
|
||||
conn.run_command(&format!("test -f {path} && echo exists || echo missing"))
|
||||
conn.run_command(&format!("test -f '{path}' && echo exists || echo missing"))
|
||||
.await
|
||||
.map(|output| output.contains("exists"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Shared config installation logic
|
||||
/// Shared config installation logic. Installs to /data/rayhunter/config.toml which resolves
|
||||
/// through the symlink to the actual data directory.
|
||||
pub async fn install_config<C: DeviceConnection>(
|
||||
conn: &mut C,
|
||||
config_path: &str,
|
||||
device_type: &str,
|
||||
reset_config: bool,
|
||||
) -> Result<()> {
|
||||
let config_path = "/data/rayhunter/config.toml";
|
||||
if reset_config || !file_exists(conn, config_path).await {
|
||||
let config = crate::CONFIG_TOML.replace(
|
||||
r#"#device = "orbic""#,
|
||||
@@ -42,6 +43,118 @@ pub async fn install_config<C: DeviceConnection>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a directory exists using a DeviceConnection
|
||||
pub async fn dir_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
|
||||
conn.run_command(&format!("test -d '{path}' && echo exists || echo missing"))
|
||||
.await
|
||||
.map(|output| output.contains("exists"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if a path is a symlink using a DeviceConnection
|
||||
pub async fn is_symlink<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
|
||||
conn.run_command(&format!("test -L '{path}' && echo yes || echo no"))
|
||||
.await
|
||||
.map(|output| output.contains("yes"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Read the target of a symlink using a DeviceConnection
|
||||
pub async fn readlink<C: DeviceConnection>(conn: &mut C, path: &str) -> Result<String> {
|
||||
// Use a prefix marker to find the actual output line, since some shells (TP-Link) echo
|
||||
// back the command and run_command appends protocol lines.
|
||||
let output = conn
|
||||
.run_command(&format!("echo RL:$(readlink '{path}')"))
|
||||
.await?;
|
||||
|
||||
for line in output.lines() {
|
||||
if let Some(target) = line.trim().strip_prefix("RL:") {
|
||||
return Ok(target.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
bail!("unexpected readlink output: {output:?}");
|
||||
}
|
||||
|
||||
/// Set up the data directory at `data_dir` and create a symlink from `/data/rayhunter` to it.
|
||||
///
|
||||
/// Handles migration from old locations:
|
||||
/// - If `/data/rayhunter` is a real directory, moves its contents to `data_dir`
|
||||
/// - If `/data/rayhunter` is a symlink to a different location, moves from the old target
|
||||
/// - If `/data/rayhunter` doesn't exist, just creates the symlink
|
||||
/// - If `/data/rayhunter` is a symlink to `data_dir`, does nothing
|
||||
pub async fn setup_data_directory<C: DeviceConnection>(conn: &mut C, data_dir: &str) -> Result<()> {
|
||||
if data_dir == "/data/rayhunter" {
|
||||
bail!("data_dir must not be /data/rayhunter");
|
||||
}
|
||||
|
||||
if data_dir.contains("'") {
|
||||
bail!("data_dir must not contain an apostrophe (')");
|
||||
}
|
||||
|
||||
// Determine where old data lives, if anywhere
|
||||
let old_data_source = if is_symlink(conn, "/data/rayhunter").await {
|
||||
let current_target = readlink(conn, "/data/rayhunter").await?;
|
||||
if current_target == data_dir {
|
||||
println!("Data directory already configured at {data_dir}");
|
||||
return Ok(());
|
||||
}
|
||||
conn.run_command("rm -f /data/rayhunter").await?;
|
||||
// The old symlink target is where data actually lives
|
||||
if dir_exists(conn, ¤t_target).await {
|
||||
Some(current_target)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if dir_exists(conn, "/data/rayhunter").await {
|
||||
if dir_exists(conn, data_dir).await {
|
||||
bail!("Both /data/rayhunter and {data_dir} exist and are directories.");
|
||||
}
|
||||
// Real directory (pre-migration Orbic state)
|
||||
Some("/data/rayhunter".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Migrate old data if present
|
||||
if let Some(old_source) = &old_data_source {
|
||||
// Stop rayhunter-daemon so it doesn't write during migration.
|
||||
// The device will be rebooted at the end of installation anyway.
|
||||
print!("Stopping rayhunter-daemon ... ");
|
||||
let _ = conn
|
||||
.run_command("/etc/init.d/rayhunter_daemon stop 2>/dev/null; true")
|
||||
.await;
|
||||
println!("ok");
|
||||
|
||||
print!("Migrating data from {old_source} to {data_dir} ... ");
|
||||
|
||||
// mv old data into its place. If source and destination are on the same filesystem,
|
||||
// this is an instant rename.
|
||||
// XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It
|
||||
// probably should, or a utility for it should exist?
|
||||
let mv_output = conn
|
||||
.run_command(&format!("mv '{old_source}' '{data_dir}' && echo MV_OK"))
|
||||
.await?;
|
||||
if mv_output.contains("MV_OK") {
|
||||
println!("ok");
|
||||
} else {
|
||||
bail!("Failed to move data from {old_source} to {data_dir}:\n{mv_output}");
|
||||
}
|
||||
} else {
|
||||
// No migration needed, just ensure the target directory exists
|
||||
conn.run_command(&format!("mkdir -p '{data_dir}'")).await?;
|
||||
}
|
||||
|
||||
// Create the symlink
|
||||
print!("Creating symlink /data/rayhunter -> {data_dir} ... ");
|
||||
conn.run_command("mkdir -p /data").await?;
|
||||
conn.run_command(&format!("ln -sf '{data_dir}' /data/rayhunter"))
|
||||
.await?;
|
||||
println!("ok");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet-based connection wrapper
|
||||
pub struct TelnetConnection {
|
||||
pub addr: SocketAddr,
|
||||
|
||||
+41
-2
@@ -6,6 +6,7 @@ use env_logger::Env;
|
||||
use anyhow::bail;
|
||||
|
||||
mod connection;
|
||||
mod moxee;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod orbic;
|
||||
mod orbic_auth;
|
||||
@@ -40,9 +41,11 @@ enum Command {
|
||||
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
OrbicUsb(InstallOrbic),
|
||||
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
|
||||
/// Install rayhunter on the Orbic RC400L via network.
|
||||
#[clap(alias = "orbic-network")]
|
||||
Orbic(OrbicNetworkArgs),
|
||||
/// Install rayhunter on the Moxee Hotspot via network.
|
||||
Moxee(MoxeeArgs),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on the Uz801.
|
||||
@@ -84,6 +87,12 @@ struct InstallTpLink {
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
|
||||
/// Override the data directory path. Defaults to /cache/rayhunter-data (or SD card path when
|
||||
/// SD card is used). Must not be /data/rayhunter, which lives on a storage partition that's
|
||||
/// too small for normal Rayhunter operation.
|
||||
#[arg(long)]
|
||||
data_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -110,6 +119,35 @@ struct OrbicNetworkArgs {
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
|
||||
/// Override the data directory path. Defaults to /data/rayhunter-data.
|
||||
/// Must not be /data/rayhunter.
|
||||
#[arg(long)]
|
||||
data_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct MoxeeArgs {
|
||||
/// IP address for Moxee admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Admin username for authentication.
|
||||
#[arg(long, default_value = "admin")]
|
||||
admin_username: String,
|
||||
|
||||
/// Admin password for authentication.
|
||||
#[arg(long)]
|
||||
admin_password: Option<String>,
|
||||
|
||||
/// Overwrite config.toml even if it already exists on the device.
|
||||
#[arg(long)]
|
||||
reset_config: bool,
|
||||
|
||||
/// Override the data directory path. Defaults to /cache/rayhunter-data.
|
||||
/// Must not be /data/rayhunter.
|
||||
#[arg(long)]
|
||||
data_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -245,7 +283,8 @@ async fn run(args: Args) -> Result<(), Error> {
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Command::OrbicUsb(args) => orbic::install(args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
|
||||
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config, args.data_dir).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||
Command::Moxee(args) => moxee::install(args).await.context("\nFailed to install rayhunter on the Moxee Hotspot")?,
|
||||
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => {
|
||||
match subcommand.command {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::MoxeeArgs;
|
||||
|
||||
pub async fn install(args: MoxeeArgs) -> Result<()> {
|
||||
let data_dir = args.data_dir.or(Some("/cache/rayhunter-data".to_string()));
|
||||
crate::orbic_network::install(
|
||||
args.admin_ip,
|
||||
args.admin_username,
|
||||
args.admin_password,
|
||||
args.reset_config,
|
||||
data_dir,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -158,13 +158,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
let mut conn = AdbConnection {
|
||||
device: &mut adb_device,
|
||||
};
|
||||
install_config(
|
||||
&mut conn,
|
||||
"/data/rayhunter/config.toml",
|
||||
"orbic",
|
||||
reset_config,
|
||||
)
|
||||
.await?;
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
}
|
||||
|
||||
install_file(
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::RAYHUNTER_DAEMON_INIT;
|
||||
use crate::connection::{TelnetConnection, install_config};
|
||||
use crate::connection::{TelnetConnection, install_config, setup_data_directory};
|
||||
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
||||
use crate::output::{eprintln, print, println};
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
@@ -22,7 +22,10 @@ struct ExploitResponse {
|
||||
}
|
||||
|
||||
async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
|
||||
let client: Client = Client::new();
|
||||
// Disable connection pooling. The Orbic's web server does not properly support
|
||||
// HTTP/1.1 keep-alive, so reusing connections causes "connection closed before
|
||||
// message completed" errors.
|
||||
let client: Client = Client::builder().pool_max_idle_per_host(0).build()?;
|
||||
|
||||
// Step 1: Get login info (priKey and session cookie)
|
||||
let login_info_response = client
|
||||
@@ -97,11 +100,14 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
|
||||
.context("Failed to parse login response")?;
|
||||
|
||||
if login_result.retcode != 0 {
|
||||
bail!("Login failed with retcode: {}", login_result.retcode);
|
||||
match login_result.retcode {
|
||||
201 => bail!("Login failed: incorrect password"),
|
||||
code => bail!("Login failed with retcode: {}", code),
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Exploit using authenticated session
|
||||
let response: ExploitResponse = client
|
||||
let exploit_result = client
|
||||
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Cookie", authenticated_cookie)
|
||||
@@ -110,14 +116,27 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
|
||||
r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.context("failed to start telnet")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to start telnet")?;
|
||||
.await;
|
||||
|
||||
if response.retcode != 0 {
|
||||
bail!("unexpected response while starting telnet: {:?}", response);
|
||||
match exploit_result {
|
||||
Ok(resp) => {
|
||||
// Try to parse response but don't fail if the server closed the connection
|
||||
match resp.json::<ExploitResponse>().await {
|
||||
Ok(response) if response.retcode != 0 => {
|
||||
bail!("unexpected response while starting telnet: {:?}", response);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
// Server likely crashed from the injection which is expected
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_connect() => {
|
||||
bail!("failed to connect to admin interface at {admin_ip}: {e}");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("exploit request failed ({e}), continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -144,6 +163,7 @@ pub async fn install(
|
||||
admin_username: String,
|
||||
admin_password: Option<String>,
|
||||
reset_config: bool,
|
||||
data_dir: Option<String>,
|
||||
) -> Result<()> {
|
||||
let Some(admin_password) = admin_password else {
|
||||
eprintln!(
|
||||
@@ -167,7 +187,8 @@ pub async fn install(
|
||||
wait_for_telnet(&admin_ip).await?;
|
||||
println!("done");
|
||||
|
||||
setup_rayhunter(&admin_ip, reset_config).await
|
||||
let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string());
|
||||
setup_rayhunter(&admin_ip, reset_config, &data_dir).await
|
||||
}
|
||||
|
||||
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
@@ -191,7 +212,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
@@ -205,7 +226,8 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", false).await?;
|
||||
let mut conn = TelnetConnection::new(addr, false);
|
||||
setup_data_directory(&mut conn, data_dir).await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
@@ -215,14 +237,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut conn = TelnetConnection::new(addr, false);
|
||||
install_config(
|
||||
&mut conn,
|
||||
"/data/rayhunter/config.toml",
|
||||
"orbic",
|
||||
reset_config,
|
||||
)
|
||||
.await?;
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
|
||||
@@ -13,7 +13,7 @@ use tokio::time::sleep;
|
||||
|
||||
use crate::TmobileArgs as Args;
|
||||
use crate::output::{print, println};
|
||||
use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
|
||||
use crate::util::{reboot_device, telnet_send_command, telnet_send_file};
|
||||
use crate::wingtech::start_telnet;
|
||||
|
||||
pub async fn install(
|
||||
@@ -92,20 +92,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
telnet_send_command(addr, "reboot", "exit code 0", true).await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
print!("Testing rayhunter ... ");
|
||||
let max_failures = 10;
|
||||
http_ok_every(
|
||||
format!("http://{admin_ip}:8080/index.html"),
|
||||
Duration::from_secs(3),
|
||||
max_failures,
|
||||
)
|
||||
.await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running at http://{admin_ip}:8080");
|
||||
reboot_device(addr, "reboot", &admin_ip).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+46
-39
@@ -18,7 +18,7 @@ use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::InstallTpLink;
|
||||
use crate::connection::{TelnetConnection, install_config};
|
||||
use crate::connection::{TelnetConnection, install_config, setup_data_directory};
|
||||
use crate::output::println;
|
||||
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
|
||||
|
||||
@@ -30,10 +30,19 @@ pub async fn main_tplink(
|
||||
admin_ip,
|
||||
sdcard_path,
|
||||
reset_config,
|
||||
data_dir,
|
||||
}: InstallTpLink,
|
||||
) -> Result<(), Error> {
|
||||
let is_v3 = start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await
|
||||
tplink_run_install(
|
||||
skip_sdcard,
|
||||
admin_ip,
|
||||
sdcard_path,
|
||||
is_v3,
|
||||
reset_config,
|
||||
data_dir,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -114,19 +123,15 @@ async fn tplink_run_install(
|
||||
mut sdcard_path: String,
|
||||
is_v3: bool,
|
||||
reset_config: bool,
|
||||
cli_data_dir: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
println!("Connecting via telnet to {admin_ip}");
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
|
||||
if skip_sdcard {
|
||||
sdcard_path = "/data/rayhunter-data".to_owned();
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("mkdir -p {sdcard_path}"),
|
||||
"exit code 0",
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
let data_dir = if let Some(dir) = cli_data_dir {
|
||||
dir
|
||||
} else if skip_sdcard {
|
||||
"/cache/rayhunter-data".to_owned()
|
||||
} else {
|
||||
if sdcard_path.is_empty() {
|
||||
let try_paths = [
|
||||
@@ -146,9 +151,12 @@ async fn tplink_run_install(
|
||||
}
|
||||
|
||||
if sdcard_path.is_empty() {
|
||||
// This error message is shown when the installer cannot figure out where this
|
||||
// device _would_ mount an SD card, regardless of whether the user did insert one.
|
||||
// If we get here, it's likely the installer was never tested for this hardware
|
||||
// version.
|
||||
anyhow::bail!(
|
||||
"Unable to determine sdcard path. Rayhunter needs a FAT-formatted SD card to function.\n\n\
|
||||
If you already inserted a FAT formatted SD card, this is a bug. Please file an issue with your hardware version.\n\n\
|
||||
"Unable to determine sdcard path. This is a bug. Please file an issue with your hardware version.\n\n\
|
||||
The installer has tried to find an empty folder to mount to on these paths: {try_paths:?}\n\
|
||||
...but none of them exist.\n\n\
|
||||
At this point, you may 'telnet {admin_ip}' and poke around in the device to figure out what went wrong yourself."
|
||||
@@ -166,49 +174,43 @@ async fn tplink_run_install(
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0", true).await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
|
||||
// Try to mount the SD card, and if that fails we assume the user didn't insert one.
|
||||
telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0", true).await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few hours. Insert one and rerun this installer, or pass --skip-sdcard")?;
|
||||
} else {
|
||||
println!("sdcard already mounted");
|
||||
}
|
||||
}
|
||||
|
||||
// there is too little space on the internal flash to store anything, but the initrd script
|
||||
// expects things to be at this location
|
||||
telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0", true).await?;
|
||||
telnet_send_command(addr, "mkdir -p /data", "exit code 0", true).await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("ln -sf {sdcard_path} /data/rayhunter"),
|
||||
"exit code 0",
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
sdcard_path
|
||||
};
|
||||
|
||||
let mut conn = TelnetConnection::new(addr, true);
|
||||
let config_path = format!("{sdcard_path}/config.toml");
|
||||
install_config(&mut conn, &config_path, "tplink", reset_config).await?;
|
||||
setup_data_directory(&mut conn, &data_dir).await?;
|
||||
|
||||
install_config(&mut conn, "tplink", reset_config).await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
&format!("{sdcard_path}/rayhunter-daemon"),
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
rayhunter_daemon_bin,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let init_script = get_rayhunter_daemon(if skip_sdcard { None } else { Some(&data_dir) });
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
get_rayhunter_daemon(&sdcard_path).as_bytes(),
|
||||
init_script.as_bytes(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
|
||||
"chmod ugo+x /data/rayhunter/rayhunter-daemon",
|
||||
"exit code 0",
|
||||
true,
|
||||
)
|
||||
@@ -368,18 +370,19 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_rayhunter_daemon(sdcard_path: &str) -> String {
|
||||
fn get_rayhunter_daemon(sdcard_path: Option<&str>) -> String {
|
||||
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
|
||||
// changing the order in which daemons are started up seems to not work reliably.
|
||||
//
|
||||
// This part of the daemon dynamically generated because we may have to eventually add logic
|
||||
// specific to a particular hardware revision here.
|
||||
crate::RAYHUNTER_DAEMON_INIT.replace(
|
||||
"#RAYHUNTER-PRESTART",
|
||||
&format!(
|
||||
"(mount /dev/mmcblk0p1 {sdcard_path} || true) 2>&1 | tee /tmp/rayhunter-mount.log"
|
||||
),
|
||||
)
|
||||
let prestart = match sdcard_path {
|
||||
Some(path) => {
|
||||
format!("(mount /dev/mmcblk0p1 {path} || true) 2>&1 | tee /tmp/rayhunter-mount.log")
|
||||
}
|
||||
None => String::new(),
|
||||
};
|
||||
crate::RAYHUNTER_DAEMON_INIT.replace("#RAYHUNTER-PRESTART", &prestart)
|
||||
}
|
||||
|
||||
/// Root the TP-Link device and open an interactive shell
|
||||
@@ -390,6 +393,10 @@ pub async fn shell(admin_ip: &str) -> Result<(), Error> {
|
||||
|
||||
#[test]
|
||||
fn test_get_rayhunter_daemon() {
|
||||
let s = get_rayhunter_daemon("/media/card");
|
||||
let s = get_rayhunter_daemon(Some("/media/card"));
|
||||
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
|
||||
|
||||
let s = get_rayhunter_daemon(None);
|
||||
assert!(!s.contains("mmcblk0p1"));
|
||||
assert!(!s.contains("#RAYHUNTER-PRESTART"));
|
||||
}
|
||||
|
||||
+62
-61
@@ -4,7 +4,6 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use nusb::Device;
|
||||
use reqwest::Client;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, timeout};
|
||||
@@ -19,53 +18,61 @@ pub async fn telnet_send_command_with_output(
|
||||
command: &str,
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<String> {
|
||||
if command.contains('\n') {
|
||||
bail!("multi-line commands are not allowed");
|
||||
}
|
||||
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
if wait_for_prompt {
|
||||
// Wait for initial '#' prompt from telnetd
|
||||
loop {
|
||||
let mut next_byte = 0;
|
||||
reader
|
||||
.read_exact(std::slice::from_mut(&mut next_byte))
|
||||
.await?;
|
||||
if next_byte == b'#' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for the shell prompt. This also consumes any telnet IAC negotiation
|
||||
// the server sends at connection start, and ensures the shell is ready
|
||||
// for input.
|
||||
while reader.read_u8().await? != b'#' {}
|
||||
}
|
||||
|
||||
writer.write_all(command.as_bytes()).await?;
|
||||
// by quoting the 'exit' here, we ensure that we do not read our own command line back as
|
||||
// "output" before we even hit enter, but the actual result of executing the echo.
|
||||
writer
|
||||
.write_all(b"; echo command done, 'exit' code $?\r\n")
|
||||
.await?;
|
||||
// This contraption is there so we clearly know where the command output starts and ends,
|
||||
// skipping telnet echoing the command back using START, and terminating the connection right
|
||||
// after the command exits.
|
||||
//
|
||||
// 'TELNET' is quoted so that when the command gets echoed back, it does not match against
|
||||
// RAYHUNTER_TELNET_COMMAND_DONE search string.
|
||||
writer.write_all(format!("echo RAYHUNTER_'TELNET'_COMMAND_START; {command}; echo RAYHUNTER_'TELNET'_COMMAND_DONE\r\n").as_bytes()).await?;
|
||||
|
||||
let mut read_buf = Vec::new();
|
||||
let _ = timeout(Duration::from_secs(10), async {
|
||||
let mut buf = [0; 4096];
|
||||
timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
let Ok(bytes_read) = reader.read(&mut buf).await else {
|
||||
let Ok(byte) = reader.read_u8().await else {
|
||||
break;
|
||||
};
|
||||
let bytes = &buf[..bytes_read];
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
read_buf.extend(bytes);
|
||||
read_buf.push(byte);
|
||||
|
||||
// when we see this string we know the command is done and can terminate.
|
||||
// even if we sent command; exit, certain "telnet-like" shells (like nc contraptions)
|
||||
// may not terminate the connection appropriately on their own.
|
||||
let response = String::from_utf8_lossy(&read_buf);
|
||||
if response.contains("command done, exit code ") {
|
||||
break;
|
||||
if byte == b'\n' {
|
||||
let response = String::from_utf8_lossy(&read_buf);
|
||||
if response.contains("RAYHUNTER_TELNET_COMMAND_DONE") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let string = String::from_utf8_lossy(&read_buf).to_string();
|
||||
Ok(string)
|
||||
.await
|
||||
.context("command timed out after 10 seconds")?;
|
||||
let string = String::from_utf8_lossy(&read_buf);
|
||||
let start = string.rfind("RAYHUNTER_TELNET_COMMAND_START");
|
||||
let end = string.rfind("RAYHUNTER_TELNET_COMMAND_DONE");
|
||||
let string = match (start, end) {
|
||||
(Some(start), Some(end)) => {
|
||||
// skip past the START marker and the trailing \r\n of the echoed command line
|
||||
let start = start + "RAYHUNTER_TELNET_COMMAND_START".len();
|
||||
string[start..end].trim_start_matches(['\r', '\n'])
|
||||
}
|
||||
_ => bail!("failed to parse command output from string: {string:?}"),
|
||||
};
|
||||
Ok(string.to_string())
|
||||
}
|
||||
|
||||
pub async fn telnet_send_command(
|
||||
@@ -74,7 +81,8 @@ pub async fn telnet_send_command(
|
||||
expected_output: &str,
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<()> {
|
||||
let output = telnet_send_command_with_output(addr, command, wait_for_prompt).await?;
|
||||
let command = format!("{command}; echo command done, exit code $?");
|
||||
let output = telnet_send_command_with_output(addr, &command, wait_for_prompt).await?;
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
@@ -93,7 +101,7 @@ pub async fn telnet_send_file(
|
||||
let handle = tokio::spawn(async move {
|
||||
telnet_send_command_with_output(
|
||||
addr,
|
||||
&format!("nc -l -p 8081 >{filename}.tmp"),
|
||||
&format!("nc -l -p 8081 2>&1 >{filename}.tmp"),
|
||||
wait_for_prompt,
|
||||
)
|
||||
.await
|
||||
@@ -121,7 +129,7 @@ pub async fn telnet_send_file(
|
||||
print!("attempt {attempts}... ");
|
||||
}
|
||||
|
||||
{
|
||||
let send_result: Result<()> = async {
|
||||
let mut stream = stream?;
|
||||
stream.write_all(payload).await?;
|
||||
|
||||
@@ -134,11 +142,23 @@ pub async fn telnet_send_file(
|
||||
// application buffers here.
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
|
||||
// ensure that stream is dropped before we wait for nc to terminate.
|
||||
drop(stream);
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
let nc_output = handle
|
||||
.await
|
||||
.context("background nc writer failed")?
|
||||
.context("background nc writer failed")?;
|
||||
|
||||
if let Err(e) = send_result {
|
||||
bail!(
|
||||
"Failed to send data to nc: {e}. nc output: '{}'",
|
||||
nc_output.trim()
|
||||
);
|
||||
}
|
||||
|
||||
handle.await??
|
||||
nc_output
|
||||
};
|
||||
|
||||
let checksum = md5::compute(payload);
|
||||
@@ -185,30 +205,11 @@ pub async fn send_file(admin_ip: &str, local_path: &str, remote_path: &str) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn http_ok_every(
|
||||
rayhunter_url: String,
|
||||
interval: Duration,
|
||||
max_failures: u32,
|
||||
) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match client.get(&rayhunter_url).send().await {
|
||||
Ok(test) => match test.status().is_success() {
|
||||
true => break,
|
||||
false => bail!(
|
||||
"request for url ({rayhunter_url}) failed with status code: {:?}",
|
||||
test.status()
|
||||
),
|
||||
},
|
||||
Err(e) => match failures > max_failures {
|
||||
true => return Err(e.into()),
|
||||
false => failures += 1,
|
||||
},
|
||||
}
|
||||
sleep(interval).await;
|
||||
}
|
||||
Ok(())
|
||||
pub async fn reboot_device(addr: SocketAddr, reboot_command: &str, admin_ip: &str) {
|
||||
println!(
|
||||
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
|
||||
);
|
||||
let _ = telnet_send_command(addr, reboot_command, "", true).await;
|
||||
}
|
||||
|
||||
/// General function to open a USB device
|
||||
|
||||
+11
-28
@@ -1,3 +1,13 @@
|
||||
use crate::WingtechArgs as Args;
|
||||
use crate::output::{print, println};
|
||||
use crate::util::{reboot_device, telnet_send_command, telnet_send_file};
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64_light::base64_encode_bytes;
|
||||
use block_padding::{Padding, Pkcs7};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
/// Installer for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
@@ -6,20 +16,6 @@
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64_light::base64_encode_bytes;
|
||||
use block_padding::{Padding, Pkcs7};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::WingtechArgs as Args;
|
||||
use crate::output::{print, println};
|
||||
use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginResponse {
|
||||
@@ -145,20 +141,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0", true).await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
print!("Testing rayhunter ... ");
|
||||
let max_failures = 10;
|
||||
http_ok_every(
|
||||
format!("http://{admin_ip}:8080/index.html"),
|
||||
Duration::from_secs(3),
|
||||
max_failures,
|
||||
)
|
||||
.await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running at http://{admin_ip}:8080");
|
||||
reboot_device(addr, "shutdown -r -t 1 now", &admin_ip).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,14 +1,16 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
|
||||
[lib]
|
||||
name = "rayhunter"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
apidocs = ["dep:utoipa"]
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.11.1"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
@@ -27,5 +29,7 @@ futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
num_enum = "0.7.4"
|
||||
utoipa = { version = "5.4.0", optional = true }
|
||||
async-compression = { version = "0.4.41", features = ["tokio", "gzip"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -17,8 +17,10 @@ use super::{
|
||||
test_analyzer::TestAnalyzer,
|
||||
};
|
||||
|
||||
/// A list of booleans which stores information about which analyzers are enabled
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct AnalyzerConfig {
|
||||
pub diagnostic_analyzer: bool,
|
||||
pub connection_redirect_2g_downgrade: bool,
|
||||
@@ -51,6 +53,7 @@ pub const REPORT_VERSION: u32 = 2;
|
||||
///
|
||||
/// Informational does not result in any alert on the display.
|
||||
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub enum EventType {
|
||||
Informational = 0,
|
||||
Low = 1,
|
||||
@@ -140,20 +143,29 @@ pub trait Analyzer {
|
||||
fn get_version(&self) -> u32;
|
||||
}
|
||||
|
||||
/// Specific information on a given analyzer
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct AnalyzerMetadata {
|
||||
/// The analyzer name
|
||||
pub name: String,
|
||||
/// A description of what the analyzer does
|
||||
pub description: String,
|
||||
/// The deployed version of the analyzer code
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
/// The metadata for an analyzed report
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct ReportMetadata {
|
||||
/// A vector array of which analyzers were in use for the analysis
|
||||
pub analyzers: Vec<AnalyzerMetadata>,
|
||||
/// The runtime metadata for rayhunter during the recording and analysis
|
||||
pub rayhunter: RuntimeMetadata,
|
||||
|
||||
/// The version of the reporting format used
|
||||
// anytime the format of the report changes, bump this by 1
|
||||
//
|
||||
// the default is 0. we consider our legacy (unversioned) heuristics to be v0 -- this'll let us
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Diag protocol serialization/deserialization
|
||||
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
@@ -113,6 +114,12 @@ impl MessagesContainer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessagesContainer> for Bytes {
|
||||
fn from(value: MessagesContainer) -> Self {
|
||||
value.to_bytes().unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct HdlcEncapsulatedMessage {
|
||||
pub len: u32,
|
||||
|
||||
@@ -29,8 +29,10 @@ pub mod diag_device;
|
||||
// re-export telcom_parser, since we use its types in our API
|
||||
pub use telcom_parser;
|
||||
|
||||
/// A list of the internal names of currently implemented devices
|
||||
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub enum Device {
|
||||
Orbic,
|
||||
Tplink,
|
||||
|
||||
+179
-57
@@ -3,8 +3,14 @@
|
||||
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
||||
//! QMDL files.
|
||||
|
||||
use std::io::{Cursor, ErrorKind};
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
|
||||
use crate::diag::{DataType, HdlcEncapsulatedMessage, MESSAGE_TERMINATOR, MessagesContainer};
|
||||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use async_compression::tokio::write::GzipEncoder;
|
||||
use futures::TryStream;
|
||||
use log::error;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
@@ -13,8 +19,8 @@ pub struct QmdlWriter<T>
|
||||
where
|
||||
T: AsyncWrite + Unpin,
|
||||
{
|
||||
writer: T,
|
||||
pub total_written: usize,
|
||||
writer: GzipEncoder<T>,
|
||||
pub total_uncompressed_bytes: usize,
|
||||
}
|
||||
|
||||
impl<T> QmdlWriter<T>
|
||||
@@ -22,50 +28,160 @@ where
|
||||
T: AsyncWrite + Unpin,
|
||||
{
|
||||
pub fn new(writer: T) -> Self {
|
||||
QmdlWriter::new_with_existing_size(writer, 0)
|
||||
}
|
||||
|
||||
pub fn new_with_existing_size(writer: T, existing_size: usize) -> Self {
|
||||
let gzip_writer = GzipEncoder::new(writer);
|
||||
QmdlWriter {
|
||||
writer,
|
||||
total_written: existing_size,
|
||||
writer: gzip_writer,
|
||||
total_uncompressed_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> {
|
||||
for msg in &container.messages {
|
||||
self.writer.write_all(&msg.data).await?;
|
||||
self.total_written += msg.data.len();
|
||||
// for a gzipped file, we can't use `msg.data.len()` to
|
||||
// determine the number of bytes written, so we have to
|
||||
// manually do a `write_all()` type loop
|
||||
let mut buf = Cursor::new(&msg.data);
|
||||
loop {
|
||||
let bytes_written = self.writer.write_buf(&mut buf).await?;
|
||||
self.writer.flush().await?;
|
||||
if bytes_written == 0 {
|
||||
break;
|
||||
}
|
||||
self.total_uncompressed_bytes += bytes_written;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn close(mut self) -> std::io::Result<()> {
|
||||
self.writer.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum QmdlReaderSource<T> {
|
||||
Compressed {
|
||||
reader: GzipDecoder<BufReader<T>>,
|
||||
eof: bool,
|
||||
},
|
||||
Uncompressed {
|
||||
reader: T,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QmdlAsyncReader<T> {
|
||||
source: QmdlReaderSource<T>,
|
||||
uncompressed_bytes_read: usize,
|
||||
max_uncompressed_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> QmdlAsyncReader<T>
|
||||
where
|
||||
T: AsyncRead,
|
||||
{
|
||||
pub fn new(reader: T, compressed: bool, max_uncompressed_bytes: Option<usize>) -> Self {
|
||||
let source = if compressed {
|
||||
QmdlReaderSource::Compressed {
|
||||
reader: GzipDecoder::new(BufReader::new(reader)),
|
||||
eof: false,
|
||||
}
|
||||
} else {
|
||||
QmdlReaderSource::Uncompressed { reader }
|
||||
};
|
||||
Self {
|
||||
source,
|
||||
uncompressed_bytes_read: 0,
|
||||
max_uncompressed_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for QmdlAsyncReader<T>
|
||||
where
|
||||
T: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
// if we've already read beyond the byte limit, return without reading
|
||||
// into the buffer, essentially signalling EOF
|
||||
if let Some(max_bytes) = self.max_uncompressed_bytes
|
||||
&& self.uncompressed_bytes_read >= max_bytes
|
||||
{
|
||||
if self.uncompressed_bytes_read > max_bytes {
|
||||
error!(
|
||||
"warning: {} bytes read, but max_bytes was {}",
|
||||
self.uncompressed_bytes_read, max_bytes
|
||||
);
|
||||
}
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let before = buf.filled().len();
|
||||
let this = self.get_mut();
|
||||
let res = match &mut this.source {
|
||||
QmdlReaderSource::Compressed { reader, eof } => {
|
||||
// if we already determined we've reached the Gzip EOF, don't read more
|
||||
if *eof {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(reader).poll_read(cx, buf) {
|
||||
// if we hit an unexpected EOF in a Gzip file, it shouldn't
|
||||
// be considered fatal, just a truncated file. mark that
|
||||
// we're done and return the result as usual
|
||||
Poll::Ready(Err(err)) if err.kind() == ErrorKind::UnexpectedEof => {
|
||||
*eof = true;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
QmdlReaderSource::Uncompressed { reader } => Pin::new(reader).poll_read(cx, buf),
|
||||
};
|
||||
|
||||
// if we read more bytes than is allowed, cap the buffer by
|
||||
// our max bytes
|
||||
let after = buf.filled().len();
|
||||
let read = after - before;
|
||||
if let Some(max_bytes) = this.max_uncompressed_bytes
|
||||
&& this.uncompressed_bytes_read + read > max_bytes
|
||||
{
|
||||
let overread = this.uncompressed_bytes_read + read - max_bytes;
|
||||
buf.set_filled(after - overread);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QmdlReader<T>
|
||||
where
|
||||
T: AsyncRead,
|
||||
{
|
||||
reader: BufReader<T>,
|
||||
bytes_read: usize,
|
||||
max_bytes: Option<usize>,
|
||||
buf_reader: BufReader<QmdlAsyncReader<T>>,
|
||||
}
|
||||
|
||||
impl<T> QmdlReader<T>
|
||||
where
|
||||
T: AsyncRead + Unpin,
|
||||
{
|
||||
pub fn new(reader: T, max_bytes: Option<usize>) -> Self {
|
||||
pub fn new(reader: T, compressed: bool, max_uncompressed_bytes: Option<usize>) -> Self {
|
||||
QmdlReader {
|
||||
reader: BufReader::new(reader),
|
||||
bytes_read: 0,
|
||||
max_bytes,
|
||||
buf_reader: BufReader::new(QmdlAsyncReader::new(
|
||||
reader,
|
||||
compressed,
|
||||
max_uncompressed_bytes,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_stream(
|
||||
&mut self,
|
||||
) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> + '_ {
|
||||
futures::stream::try_unfold(self, |reader| async {
|
||||
pub fn as_stream(self) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> {
|
||||
futures::stream::try_unfold(self, |mut reader| async {
|
||||
let maybe_container = reader.get_next_messages_container().await?;
|
||||
match maybe_container {
|
||||
Some(container) => Ok(Some((container, reader))),
|
||||
@@ -77,22 +193,16 @@ where
|
||||
pub async fn get_next_messages_container(
|
||||
&mut self,
|
||||
) -> Result<Option<MessagesContainer>, std::io::Error> {
|
||||
if let Some(max_bytes) = self.max_bytes
|
||||
&& self.bytes_read >= max_bytes
|
||||
let mut buf = Vec::new();
|
||||
if self
|
||||
.buf_reader
|
||||
.read_until(MESSAGE_TERMINATOR, &mut buf)
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
if self.bytes_read > max_bytes {
|
||||
error!(
|
||||
"warning: {} bytes read, but max_bytes was {}",
|
||||
self.bytes_read, max_bytes
|
||||
);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let bytes_read = self.reader.read_until(MESSAGE_TERMINATOR, &mut buf).await?;
|
||||
self.bytes_read += bytes_read;
|
||||
|
||||
// Since QMDL is just a flat list of messages, we can't actually
|
||||
// reproduce the container structure they came from in the original
|
||||
// read. So we'll just pretend that all containers had exactly one
|
||||
@@ -102,13 +212,26 @@ where
|
||||
data_type: DataType::UserSpace,
|
||||
num_messages: 1,
|
||||
messages: vec![HdlcEncapsulatedMessage {
|
||||
len: bytes_read as u32,
|
||||
len: buf.len() as u32,
|
||||
data: buf,
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for QmdlReader<T>
|
||||
where
|
||||
T: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().buf_reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
@@ -160,7 +283,7 @@ mod test {
|
||||
#[tokio::test]
|
||||
async fn test_unbounded_qmdl_reader() {
|
||||
let mut buf = Cursor::new(get_test_message_bytes());
|
||||
let mut reader = QmdlReader::new(&mut buf, None);
|
||||
let mut reader = QmdlReader::new(&mut buf, false, None);
|
||||
let expected_messages = get_test_messages();
|
||||
for message in expected_messages {
|
||||
let expected_container = MessagesContainer {
|
||||
@@ -183,7 +306,7 @@ mod test {
|
||||
let mut expected_messages = get_test_messages();
|
||||
let limit = expected_messages[0].len + expected_messages[1].len;
|
||||
|
||||
let mut reader = QmdlReader::new(&mut buf, Some(limit as usize));
|
||||
let mut reader = QmdlReader::new(&mut buf, false, Some(limit as usize));
|
||||
for message in expected_messages.drain(0..2) {
|
||||
let expected_container = MessagesContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
@@ -201,29 +324,22 @@ mod test {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_qmdl_writer() {
|
||||
/// Writes the test containers to a QmdlWriter, optionally finishing the
|
||||
/// gzip stream with a footer. Then, attempts to decompress the buffer with
|
||||
/// a QmdlWriter, asserting that the containers match what's expected.
|
||||
async fn run_compressed_reading_and_writing_tests(do_close: bool) {
|
||||
let containers = get_test_containers();
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
let expected_containers = get_test_containers();
|
||||
for container in &expected_containers {
|
||||
writer.write_container(container).await.unwrap();
|
||||
{
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
for container in &containers {
|
||||
writer.write_container(&container).await.unwrap();
|
||||
}
|
||||
if do_close {
|
||||
writer.close().await.unwrap();
|
||||
}
|
||||
}
|
||||
assert_eq!(writer.total_written, buf.len());
|
||||
assert_eq!(buf, get_test_message_bytes());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_writing_and_reading() {
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = QmdlWriter::new(&mut buf);
|
||||
let expected_containers = get_test_containers();
|
||||
for container in &expected_containers {
|
||||
writer.write_container(container).await.unwrap();
|
||||
}
|
||||
|
||||
let limit = Some(buf.len());
|
||||
let mut reader = QmdlReader::new(Cursor::new(&mut buf), limit);
|
||||
let mut reader = QmdlReader::new(Cursor::new(buf), true, None);
|
||||
let expected_messages = get_test_messages();
|
||||
for message in expected_messages {
|
||||
let expected_container = MessagesContainer {
|
||||
@@ -241,4 +357,10 @@ mod test {
|
||||
Ok(None)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compressed_reading_and_writing() {
|
||||
run_compressed_reading_and_writing_tests(true).await;
|
||||
run_compressed_reading_and_writing_tests(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use nix::sys::utsname::uname;
|
||||
|
||||
/// Expose binary and system information.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct RuntimeMetadata {
|
||||
/// The cargo package version from this library's cargo.toml, e.g., "1.2.3".
|
||||
pub rayhunter_version: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Build Rayhunter from source for development.
|
||||
# Prerequisites: Rust (rustup) and Node.js (npm).
|
||||
#
|
||||
# Usage: ./scripts/build-dev.sh [build|frontend|check]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
check_dependencies() {
|
||||
local missing=0
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "Error: cargo not found. Install Rust via https://www.rust-lang.org/tools/install"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "Error: npm not found. Install Node.js via https://docs.npmjs.com/downloading-and-installing-node-js-and-npm"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [ "$missing" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure the ARM cross-compilation target is installed
|
||||
if ! rustup target list --installed | grep -q "armv7-unknown-linux-musleabihf"; then
|
||||
echo "Installing ARM target (armv7-unknown-linux-musleabihf)..."
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
fi
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
echo "Building web frontend..."
|
||||
pushd daemon/web > /dev/null
|
||||
npm install
|
||||
npm run build
|
||||
popd > /dev/null
|
||||
}
|
||||
|
||||
build_daemon() {
|
||||
echo "Building daemon..."
|
||||
cargo build-daemon-firmware-devel
|
||||
|
||||
echo "Building rootshell..."
|
||||
cargo build-rootshell-firmware-devel
|
||||
}
|
||||
|
||||
COMMAND="${1:-build}"
|
||||
|
||||
case "$COMMAND" in
|
||||
build)
|
||||
check_dependencies
|
||||
build_frontend
|
||||
build_daemon
|
||||
echo ""
|
||||
echo "Build complete! To install to a device, run:"
|
||||
echo " ./scripts/install-dev.sh <device>"
|
||||
echo ""
|
||||
echo "Replace <device> with your device type (e.g. orbic, tplink)."
|
||||
;;
|
||||
frontend)
|
||||
build_frontend
|
||||
;;
|
||||
check)
|
||||
check_dependencies
|
||||
;;
|
||||
help|--help|-h)
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build Build frontend, daemon, and rootshell (default)"
|
||||
echo " frontend Build only the web frontend"
|
||||
echo " check Check dependencies only"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $COMMAND"
|
||||
echo "Run '$0 help' for usage."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Install a development build of Rayhunter to a device.
|
||||
# Run ./scripts/build-dev.sh first.
|
||||
#
|
||||
# Usage: ./scripts/install-dev.sh <device> [options...]
|
||||
# Example: ./scripts/install-dev.sh orbic --admin-password mypass
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
cargo run -p installer --bin installer -- "$@"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user