python-rayhunter 2025.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_rayhunter-2025.3.1/LICENSE +21 -0
- python_rayhunter-2025.3.1/PKG-INFO +173 -0
- python_rayhunter-2025.3.1/README.md +154 -0
- python_rayhunter-2025.3.1/pyproject.toml +35 -0
- python_rayhunter-2025.3.1/requirements.txt +1 -0
- python_rayhunter-2025.3.1/setup.cfg +4 -0
- python_rayhunter-2025.3.1/src/python_rayhunter.egg-info/PKG-INFO +173 -0
- python_rayhunter-2025.3.1/src/python_rayhunter.egg-info/SOURCES.txt +13 -0
- python_rayhunter-2025.3.1/src/python_rayhunter.egg-info/dependency_links.txt +1 -0
- python_rayhunter-2025.3.1/src/python_rayhunter.egg-info/requires.txt +1 -0
- python_rayhunter-2025.3.1/src/python_rayhunter.egg-info/top_level.txt +1 -0
- python_rayhunter-2025.3.1/src/rayhunter/__init__.py +6 -0
- python_rayhunter-2025.3.1/src/rayhunter/api.py +108 -0
- python_rayhunter-2025.3.1/src/rayhunter/manifest.py +47 -0
- python_rayhunter-2025.3.1/src/rayhunter/system_stats.py +91 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jeff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-rayhunter
|
|
3
|
+
Version: 2025.3.1
|
|
4
|
+
Summary: Unofficial Python bindings for EFF's Rayhunter API
|
|
5
|
+
Author-email: UltraSunshine <python-rayhunter.hedge098@passfwd.com>
|
|
6
|
+
Maintainer-email: UltraSunshine <python-rayhunter.hedge098@passfwd.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/UltraSunshine/python-rayhunter
|
|
8
|
+
Project-URL: Documentation, https://python-rayhunter.readthedocs.io/en/latest/index.html
|
|
9
|
+
Project-URL: Repository, https://github.com/UltraSunshine/python-rayhunter.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/UltraSunshine/python-rayhunter/issues
|
|
11
|
+
Keywords: rayhunter,lte,orbic,qualcomm,pcap,qmdl
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.32.2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# python-rayhunter
|
|
21
|
+
|
|
22
|
+
Unofficial Python bindings for EFF's [Rayhunter](https://github.com/EFForg/rayhunter) API.
|
|
23
|
+
|
|
24
|
+
> **Note:** This project is not affiliated with the EFF or the Rayhunter project.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
Full documentation can be found on [Read The Docs](https://python-rayhunter.readthedocs.io/en/latest/index.html). Please read them, I worked hard on them.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python >= 3.11
|
|
34
|
+
- [requests](https://pypi.org/project/requests/) >= 2.32.2
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install python-rayhunter
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from rayhunter import RayhunterApi
|
|
46
|
+
|
|
47
|
+
api = RayhunterApi(hostname="192.168.1.1", port=8080)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Check recording status
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
if api.active_recording:
|
|
56
|
+
print("A recording is currently in progress")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Fetch the QMDL manifest
|
|
60
|
+
|
|
61
|
+
The manifest lists all capture files available on the device, plus the active capture (if any).
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
manifest = api.get_manifest()
|
|
65
|
+
|
|
66
|
+
for entry in manifest.entries:
|
|
67
|
+
print(f"{entry.name} — started {entry.start_time}, size {entry.qmdl_size_bytes} bytes")
|
|
68
|
+
|
|
69
|
+
if manifest.current_entry:
|
|
70
|
+
print(f"Active capture: {manifest.current_entry.name}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Download capture files
|
|
74
|
+
|
|
75
|
+
Use the filenames from the manifest to download files.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Raw QMDL capture data
|
|
79
|
+
qmdl_data = api.get_qmdl_file("capture.qmdl")
|
|
80
|
+
|
|
81
|
+
# PCAP file (dynamically generated from QMDL by the Rayhunter binary)
|
|
82
|
+
pcap_data = api.get_pcap_file("capture.qmdl")
|
|
83
|
+
|
|
84
|
+
# Analysis report
|
|
85
|
+
report_data = api.get_analysis_report_file("capture.qmdl")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Control recordings
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# Start a new recording (stops any active recording first)
|
|
92
|
+
api.start_recording()
|
|
93
|
+
|
|
94
|
+
# Stop the current recording
|
|
95
|
+
api.stop_recording()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### System statistics
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
stats = api.system_stats()
|
|
102
|
+
|
|
103
|
+
disk = stats.disk_stats
|
|
104
|
+
print(f"Partition: {disk.partition} ({disk.mounted_on})")
|
|
105
|
+
print(f"Disk usage: {disk.used_size}/{disk.total_size} bytes ({disk.used_percent}% used)")
|
|
106
|
+
|
|
107
|
+
mem = stats.memory_stats
|
|
108
|
+
print(f"Memory: {mem.used}/{mem.total} bytes used, {mem.free} bytes free")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API Reference
|
|
112
|
+
|
|
113
|
+
### `RayhunterApi(hostname, port)`
|
|
114
|
+
|
|
115
|
+
The main client for interacting with the Rayhunter API.
|
|
116
|
+
|
|
117
|
+
| Method | Returns | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `active_recording` | `bool` | `True` if a recording is currently in progress |
|
|
120
|
+
| `get_manifest()` | `QmdlManifest` | Fetch the QMDL manifest from the device |
|
|
121
|
+
| `get_qmdl_file(filename)` | `bytes` | Download a raw QMDL capture file |
|
|
122
|
+
| `get_pcap_file(filename)` | `bytes` | Download a PCAP file (generated on demand) |
|
|
123
|
+
| `get_analysis_report_file(filename)` | `bytes` | Download the analysis report for a capture |
|
|
124
|
+
| `start_recording()` | — | Start a new recording |
|
|
125
|
+
| `stop_recording()` | — | Stop the active recording |
|
|
126
|
+
| `system_stats()` | `SystemStats` | Fetch disk and memory utilisation stats |
|
|
127
|
+
|
|
128
|
+
### `QmdlManifest`
|
|
129
|
+
|
|
130
|
+
| Attribute | Type | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `entries` | `List[QmdlManifestEntry]` | All finalised capture files on the device |
|
|
133
|
+
| `current_entry` | `Optional[QmdlManifestEntry]` | The active capture, or `None` |
|
|
134
|
+
|
|
135
|
+
### `QmdlManifestEntry`
|
|
136
|
+
|
|
137
|
+
| Attribute | Type | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `name` | `str` | Capture file name |
|
|
140
|
+
| `start_time` | `str` | Timestamp when the capture started |
|
|
141
|
+
| `last_message_time` | `str` | Timestamp of the last captured message |
|
|
142
|
+
| `qmdl_size_bytes` | `int` | Size of the QMDL file in bytes |
|
|
143
|
+
| `analysis_size_bytes` | `int` | Size of the associated analysis file in bytes |
|
|
144
|
+
|
|
145
|
+
### `SystemStats`
|
|
146
|
+
|
|
147
|
+
| Attribute | Type | Description |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `disk_stats` | `DiskStats` | Disk usage information |
|
|
150
|
+
| `memory_stats` | `MemoryStats` | Memory usage information |
|
|
151
|
+
|
|
152
|
+
### `DiskStats`
|
|
153
|
+
|
|
154
|
+
| Attribute | Type | Description |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `partition` | `str` | Partition Rayhunter is mounted on (e.g. `ubi0:usrfs`) |
|
|
157
|
+
| `total_size` | `int` | Total disk size in bytes |
|
|
158
|
+
| `used_size` | `int` | Used disk space in bytes |
|
|
159
|
+
| `available_size` | `int` | Available disk space in bytes |
|
|
160
|
+
| `used_percent` | `int` | Percentage of disk space in use |
|
|
161
|
+
| `mounted_on` | `str` | Mount point (e.g. `/data`) |
|
|
162
|
+
|
|
163
|
+
### `MemoryStats`
|
|
164
|
+
|
|
165
|
+
| Attribute | Type | Description |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `total` | `int` | Total memory in bytes |
|
|
168
|
+
| `used` | `int` | Used memory in bytes |
|
|
169
|
+
| `free` | `int` | Free memory in bytes |
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
Since "do what you'd like and don't blame me," isn't necessarily legally binding, this repository uses the [MIT LICENSE](LICENSE).
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# python-rayhunter
|
|
2
|
+
|
|
3
|
+
Unofficial Python bindings for EFF's [Rayhunter](https://github.com/EFForg/rayhunter) API.
|
|
4
|
+
|
|
5
|
+
> **Note:** This project is not affiliated with the EFF or the Rayhunter project.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Documentation
|
|
9
|
+
|
|
10
|
+
Full documentation can be found on [Read The Docs](https://python-rayhunter.readthedocs.io/en/latest/index.html). Please read them, I worked hard on them.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python >= 3.11
|
|
15
|
+
- [requests](https://pypi.org/project/requests/) >= 2.32.2
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install python-rayhunter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from rayhunter import RayhunterApi
|
|
27
|
+
|
|
28
|
+
api = RayhunterApi(hostname="192.168.1.1", port=8080)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Check recording status
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
if api.active_recording:
|
|
37
|
+
print("A recording is currently in progress")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Fetch the QMDL manifest
|
|
41
|
+
|
|
42
|
+
The manifest lists all capture files available on the device, plus the active capture (if any).
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
manifest = api.get_manifest()
|
|
46
|
+
|
|
47
|
+
for entry in manifest.entries:
|
|
48
|
+
print(f"{entry.name} — started {entry.start_time}, size {entry.qmdl_size_bytes} bytes")
|
|
49
|
+
|
|
50
|
+
if manifest.current_entry:
|
|
51
|
+
print(f"Active capture: {manifest.current_entry.name}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Download capture files
|
|
55
|
+
|
|
56
|
+
Use the filenames from the manifest to download files.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Raw QMDL capture data
|
|
60
|
+
qmdl_data = api.get_qmdl_file("capture.qmdl")
|
|
61
|
+
|
|
62
|
+
# PCAP file (dynamically generated from QMDL by the Rayhunter binary)
|
|
63
|
+
pcap_data = api.get_pcap_file("capture.qmdl")
|
|
64
|
+
|
|
65
|
+
# Analysis report
|
|
66
|
+
report_data = api.get_analysis_report_file("capture.qmdl")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Control recordings
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Start a new recording (stops any active recording first)
|
|
73
|
+
api.start_recording()
|
|
74
|
+
|
|
75
|
+
# Stop the current recording
|
|
76
|
+
api.stop_recording()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### System statistics
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
stats = api.system_stats()
|
|
83
|
+
|
|
84
|
+
disk = stats.disk_stats
|
|
85
|
+
print(f"Partition: {disk.partition} ({disk.mounted_on})")
|
|
86
|
+
print(f"Disk usage: {disk.used_size}/{disk.total_size} bytes ({disk.used_percent}% used)")
|
|
87
|
+
|
|
88
|
+
mem = stats.memory_stats
|
|
89
|
+
print(f"Memory: {mem.used}/{mem.total} bytes used, {mem.free} bytes free")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
### `RayhunterApi(hostname, port)`
|
|
95
|
+
|
|
96
|
+
The main client for interacting with the Rayhunter API.
|
|
97
|
+
|
|
98
|
+
| Method | Returns | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `active_recording` | `bool` | `True` if a recording is currently in progress |
|
|
101
|
+
| `get_manifest()` | `QmdlManifest` | Fetch the QMDL manifest from the device |
|
|
102
|
+
| `get_qmdl_file(filename)` | `bytes` | Download a raw QMDL capture file |
|
|
103
|
+
| `get_pcap_file(filename)` | `bytes` | Download a PCAP file (generated on demand) |
|
|
104
|
+
| `get_analysis_report_file(filename)` | `bytes` | Download the analysis report for a capture |
|
|
105
|
+
| `start_recording()` | — | Start a new recording |
|
|
106
|
+
| `stop_recording()` | — | Stop the active recording |
|
|
107
|
+
| `system_stats()` | `SystemStats` | Fetch disk and memory utilisation stats |
|
|
108
|
+
|
|
109
|
+
### `QmdlManifest`
|
|
110
|
+
|
|
111
|
+
| Attribute | Type | Description |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `entries` | `List[QmdlManifestEntry]` | All finalised capture files on the device |
|
|
114
|
+
| `current_entry` | `Optional[QmdlManifestEntry]` | The active capture, or `None` |
|
|
115
|
+
|
|
116
|
+
### `QmdlManifestEntry`
|
|
117
|
+
|
|
118
|
+
| Attribute | Type | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `name` | `str` | Capture file name |
|
|
121
|
+
| `start_time` | `str` | Timestamp when the capture started |
|
|
122
|
+
| `last_message_time` | `str` | Timestamp of the last captured message |
|
|
123
|
+
| `qmdl_size_bytes` | `int` | Size of the QMDL file in bytes |
|
|
124
|
+
| `analysis_size_bytes` | `int` | Size of the associated analysis file in bytes |
|
|
125
|
+
|
|
126
|
+
### `SystemStats`
|
|
127
|
+
|
|
128
|
+
| Attribute | Type | Description |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `disk_stats` | `DiskStats` | Disk usage information |
|
|
131
|
+
| `memory_stats` | `MemoryStats` | Memory usage information |
|
|
132
|
+
|
|
133
|
+
### `DiskStats`
|
|
134
|
+
|
|
135
|
+
| Attribute | Type | Description |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `partition` | `str` | Partition Rayhunter is mounted on (e.g. `ubi0:usrfs`) |
|
|
138
|
+
| `total_size` | `int` | Total disk size in bytes |
|
|
139
|
+
| `used_size` | `int` | Used disk space in bytes |
|
|
140
|
+
| `available_size` | `int` | Available disk space in bytes |
|
|
141
|
+
| `used_percent` | `int` | Percentage of disk space in use |
|
|
142
|
+
| `mounted_on` | `str` | Mount point (e.g. `/data`) |
|
|
143
|
+
|
|
144
|
+
### `MemoryStats`
|
|
145
|
+
|
|
146
|
+
| Attribute | Type | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `total` | `int` | Total memory in bytes |
|
|
149
|
+
| `used` | `int` | Used memory in bytes |
|
|
150
|
+
| `free` | `int` | Free memory in bytes |
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
Since "do what you'd like and don't blame me," isn't necessarily legally binding, this repository uses the [MIT LICENSE](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-rayhunter"
|
|
3
|
+
|
|
4
|
+
version = "2025.3.1"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "UltraSunshine", email = "python-rayhunter.hedge098@passfwd.com"},
|
|
9
|
+
]
|
|
10
|
+
maintainers = [
|
|
11
|
+
{name = "UltraSunshine", email = "python-rayhunter.hedge098@passfwd.com"}
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
description = "Unofficial Python bindings for EFF's Rayhunter API"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
license-files = ["LICEN[CS]E*"]
|
|
17
|
+
|
|
18
|
+
keywords = ["rayhunter", "lte", "orbic", "qualcomm", "pcap", "qmdl"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Programming Language :: Python"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dynamic = ["dependencies"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.dynamic]
|
|
27
|
+
dependencies = {file = ["requirements.txt"]}
|
|
28
|
+
optional-dependencies = {dev = { file = ["requirements-dev.txt"] }}
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/UltraSunshine/python-rayhunter"
|
|
32
|
+
Documentation = "https://python-rayhunter.readthedocs.io/en/latest/index.html"
|
|
33
|
+
Repository = "https://github.com/UltraSunshine/python-rayhunter.git"
|
|
34
|
+
"Bug Tracker" = "https://github.com/UltraSunshine/python-rayhunter/issues"
|
|
35
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.32.2
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-rayhunter
|
|
3
|
+
Version: 2025.3.1
|
|
4
|
+
Summary: Unofficial Python bindings for EFF's Rayhunter API
|
|
5
|
+
Author-email: UltraSunshine <python-rayhunter.hedge098@passfwd.com>
|
|
6
|
+
Maintainer-email: UltraSunshine <python-rayhunter.hedge098@passfwd.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/UltraSunshine/python-rayhunter
|
|
8
|
+
Project-URL: Documentation, https://python-rayhunter.readthedocs.io/en/latest/index.html
|
|
9
|
+
Project-URL: Repository, https://github.com/UltraSunshine/python-rayhunter.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/UltraSunshine/python-rayhunter/issues
|
|
11
|
+
Keywords: rayhunter,lte,orbic,qualcomm,pcap,qmdl
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.32.2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# python-rayhunter
|
|
21
|
+
|
|
22
|
+
Unofficial Python bindings for EFF's [Rayhunter](https://github.com/EFForg/rayhunter) API.
|
|
23
|
+
|
|
24
|
+
> **Note:** This project is not affiliated with the EFF or the Rayhunter project.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
Full documentation can be found on [Read The Docs](https://python-rayhunter.readthedocs.io/en/latest/index.html). Please read them, I worked hard on them.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python >= 3.11
|
|
34
|
+
- [requests](https://pypi.org/project/requests/) >= 2.32.2
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install python-rayhunter
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from rayhunter import RayhunterApi
|
|
46
|
+
|
|
47
|
+
api = RayhunterApi(hostname="192.168.1.1", port=8080)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Check recording status
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
if api.active_recording:
|
|
56
|
+
print("A recording is currently in progress")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Fetch the QMDL manifest
|
|
60
|
+
|
|
61
|
+
The manifest lists all capture files available on the device, plus the active capture (if any).
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
manifest = api.get_manifest()
|
|
65
|
+
|
|
66
|
+
for entry in manifest.entries:
|
|
67
|
+
print(f"{entry.name} — started {entry.start_time}, size {entry.qmdl_size_bytes} bytes")
|
|
68
|
+
|
|
69
|
+
if manifest.current_entry:
|
|
70
|
+
print(f"Active capture: {manifest.current_entry.name}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Download capture files
|
|
74
|
+
|
|
75
|
+
Use the filenames from the manifest to download files.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Raw QMDL capture data
|
|
79
|
+
qmdl_data = api.get_qmdl_file("capture.qmdl")
|
|
80
|
+
|
|
81
|
+
# PCAP file (dynamically generated from QMDL by the Rayhunter binary)
|
|
82
|
+
pcap_data = api.get_pcap_file("capture.qmdl")
|
|
83
|
+
|
|
84
|
+
# Analysis report
|
|
85
|
+
report_data = api.get_analysis_report_file("capture.qmdl")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Control recordings
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# Start a new recording (stops any active recording first)
|
|
92
|
+
api.start_recording()
|
|
93
|
+
|
|
94
|
+
# Stop the current recording
|
|
95
|
+
api.stop_recording()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### System statistics
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
stats = api.system_stats()
|
|
102
|
+
|
|
103
|
+
disk = stats.disk_stats
|
|
104
|
+
print(f"Partition: {disk.partition} ({disk.mounted_on})")
|
|
105
|
+
print(f"Disk usage: {disk.used_size}/{disk.total_size} bytes ({disk.used_percent}% used)")
|
|
106
|
+
|
|
107
|
+
mem = stats.memory_stats
|
|
108
|
+
print(f"Memory: {mem.used}/{mem.total} bytes used, {mem.free} bytes free")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API Reference
|
|
112
|
+
|
|
113
|
+
### `RayhunterApi(hostname, port)`
|
|
114
|
+
|
|
115
|
+
The main client for interacting with the Rayhunter API.
|
|
116
|
+
|
|
117
|
+
| Method | Returns | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `active_recording` | `bool` | `True` if a recording is currently in progress |
|
|
120
|
+
| `get_manifest()` | `QmdlManifest` | Fetch the QMDL manifest from the device |
|
|
121
|
+
| `get_qmdl_file(filename)` | `bytes` | Download a raw QMDL capture file |
|
|
122
|
+
| `get_pcap_file(filename)` | `bytes` | Download a PCAP file (generated on demand) |
|
|
123
|
+
| `get_analysis_report_file(filename)` | `bytes` | Download the analysis report for a capture |
|
|
124
|
+
| `start_recording()` | — | Start a new recording |
|
|
125
|
+
| `stop_recording()` | — | Stop the active recording |
|
|
126
|
+
| `system_stats()` | `SystemStats` | Fetch disk and memory utilisation stats |
|
|
127
|
+
|
|
128
|
+
### `QmdlManifest`
|
|
129
|
+
|
|
130
|
+
| Attribute | Type | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `entries` | `List[QmdlManifestEntry]` | All finalised capture files on the device |
|
|
133
|
+
| `current_entry` | `Optional[QmdlManifestEntry]` | The active capture, or `None` |
|
|
134
|
+
|
|
135
|
+
### `QmdlManifestEntry`
|
|
136
|
+
|
|
137
|
+
| Attribute | Type | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `name` | `str` | Capture file name |
|
|
140
|
+
| `start_time` | `str` | Timestamp when the capture started |
|
|
141
|
+
| `last_message_time` | `str` | Timestamp of the last captured message |
|
|
142
|
+
| `qmdl_size_bytes` | `int` | Size of the QMDL file in bytes |
|
|
143
|
+
| `analysis_size_bytes` | `int` | Size of the associated analysis file in bytes |
|
|
144
|
+
|
|
145
|
+
### `SystemStats`
|
|
146
|
+
|
|
147
|
+
| Attribute | Type | Description |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `disk_stats` | `DiskStats` | Disk usage information |
|
|
150
|
+
| `memory_stats` | `MemoryStats` | Memory usage information |
|
|
151
|
+
|
|
152
|
+
### `DiskStats`
|
|
153
|
+
|
|
154
|
+
| Attribute | Type | Description |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `partition` | `str` | Partition Rayhunter is mounted on (e.g. `ubi0:usrfs`) |
|
|
157
|
+
| `total_size` | `int` | Total disk size in bytes |
|
|
158
|
+
| `used_size` | `int` | Used disk space in bytes |
|
|
159
|
+
| `available_size` | `int` | Available disk space in bytes |
|
|
160
|
+
| `used_percent` | `int` | Percentage of disk space in use |
|
|
161
|
+
| `mounted_on` | `str` | Mount point (e.g. `/data`) |
|
|
162
|
+
|
|
163
|
+
### `MemoryStats`
|
|
164
|
+
|
|
165
|
+
| Attribute | Type | Description |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `total` | `int` | Total memory in bytes |
|
|
168
|
+
| `used` | `int` | Used memory in bytes |
|
|
169
|
+
| `free` | `int` | Free memory in bytes |
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
Since "do what you'd like and don't blame me," isn't necessarily legally binding, this repository uses the [MIT LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.txt
|
|
5
|
+
src/python_rayhunter.egg-info/PKG-INFO
|
|
6
|
+
src/python_rayhunter.egg-info/SOURCES.txt
|
|
7
|
+
src/python_rayhunter.egg-info/dependency_links.txt
|
|
8
|
+
src/python_rayhunter.egg-info/requires.txt
|
|
9
|
+
src/python_rayhunter.egg-info/top_level.txt
|
|
10
|
+
src/rayhunter/__init__.py
|
|
11
|
+
src/rayhunter/api.py
|
|
12
|
+
src/rayhunter/manifest.py
|
|
13
|
+
src/rayhunter/system_stats.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.32.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rayhunter
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import requests
|
|
4
|
+
import urllib.parse
|
|
5
|
+
|
|
6
|
+
from .manifest import QmdlManifest
|
|
7
|
+
from .system_stats import SystemStats
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RayhunterApi:
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def active_recording(self) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Check the manifest file to determine if there's a recording in progress.
|
|
16
|
+
:returns: True if there's a "current_entry" in the manifest, else False
|
|
17
|
+
"""
|
|
18
|
+
manifest = self.get_manifest()
|
|
19
|
+
return manifest.current_entry is not None
|
|
20
|
+
|
|
21
|
+
def __init__(self, hostname: str, port: int):
|
|
22
|
+
self._url = f"http://{hostname}:{port}/"
|
|
23
|
+
|
|
24
|
+
def _get_file_content(self, api_endpoint: str) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Stream a file from the given API endpoint into memory.
|
|
27
|
+
:param api_endpoint: The endpoint from which to retrieve a file
|
|
28
|
+
:returns: The contents of the file (bytes)
|
|
29
|
+
"""
|
|
30
|
+
file_content = io.BytesIO()
|
|
31
|
+
file_url = urllib.parse.urljoin(self._url, api_endpoint)
|
|
32
|
+
logging.info(f"Downloading file from: {file_url}")
|
|
33
|
+
response = requests.get(file_url, stream=True)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
for chunk in response.iter_content(chunk_size=4096):
|
|
36
|
+
file_content.write(chunk)
|
|
37
|
+
file_content.seek(0)
|
|
38
|
+
return file_content.read()
|
|
39
|
+
|
|
40
|
+
def get_manifest(self) -> QmdlManifest:
|
|
41
|
+
"""
|
|
42
|
+
Fetch a copy of the QMDL manifest, used to track the names of previous and active recordings.
|
|
43
|
+
:returns: An instance of `QmdlManifest` populated from the target device
|
|
44
|
+
"""
|
|
45
|
+
manifest_url = urllib.parse.urljoin(self._url, "/api/qmdl-manifest")
|
|
46
|
+
logging.info(f"Fetching manifest from: {manifest_url}")
|
|
47
|
+
response = requests.get(manifest_url)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return QmdlManifest.from_dict(response.json())
|
|
50
|
+
|
|
51
|
+
def get_analysis_report_file(self, filename: str) -> bytes:
|
|
52
|
+
"""
|
|
53
|
+
Fetch a copy of the analysis report for a given capture. Use `get_manifest` to identify capture names.
|
|
54
|
+
:param filename: The capture file name
|
|
55
|
+
:returns: The contents of the analysis report file (bytes)
|
|
56
|
+
"""
|
|
57
|
+
logging.info(f"Fetching analysis report for capture: {filename}")
|
|
58
|
+
api_endpoint = f"/api/analysis-report/{filename}"
|
|
59
|
+
return self._get_file_content(api_endpoint)
|
|
60
|
+
|
|
61
|
+
def get_pcap_file(self, filename: str) -> bytes:
|
|
62
|
+
"""
|
|
63
|
+
Fetch a copy of the pcap file for a given capture. PCAP is dynamically generated from QMDL by the Rayhunter binary when this API is called.
|
|
64
|
+
:param filename: The capture file name (found in manifest)
|
|
65
|
+
:returns: The contents of the pcap file (bytes)
|
|
66
|
+
"""
|
|
67
|
+
logging.info(f"Fetching PCAP file for capture: {filename}")
|
|
68
|
+
api_endpoint = f"/api/pcap/{filename}"
|
|
69
|
+
return self._get_file_content(api_endpoint)
|
|
70
|
+
|
|
71
|
+
def get_qmdl_file(self, filename: str) -> bytes:
|
|
72
|
+
"""
|
|
73
|
+
Fetch a copy of the given QMDL file. Use `get_manifest` to identify QMDL capture names.
|
|
74
|
+
:param filenae: The QMDL file name (found in manifest)
|
|
75
|
+
:returns: The contents of the QMDL file (bytes)
|
|
76
|
+
"""
|
|
77
|
+
logging.info(f"Fetching QDML file for capture: {filename}")
|
|
78
|
+
api_endpoint = f"/api/qmdl/{filename}"
|
|
79
|
+
return self._get_file_content(api_endpoint)
|
|
80
|
+
|
|
81
|
+
def start_recording(self):
|
|
82
|
+
"""
|
|
83
|
+
Start a new recording. Stops the active recording and starts a new one if this device is already recording.
|
|
84
|
+
"""
|
|
85
|
+
start_recording_url = urllib.parse.urljoin(self._url, "/api/start-recording")
|
|
86
|
+
logging.info(f"Starting recording with POST request to: {start_recording_url}")
|
|
87
|
+
response = requests.post(start_recording_url)
|
|
88
|
+
response.raise_for_status()
|
|
89
|
+
|
|
90
|
+
def stop_recording(self):
|
|
91
|
+
"""
|
|
92
|
+
Stop an active recording. Throws a 500 error if there is no active recording.
|
|
93
|
+
"""
|
|
94
|
+
stop_recording_url = urllib.parse.urljoin(self._url, "/api/stop-recording")
|
|
95
|
+
logging.info(f"Stopping recording with POST request to: {stop_recording_url}")
|
|
96
|
+
response = requests.post(stop_recording_url)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
|
|
99
|
+
def system_stats(self):
|
|
100
|
+
"""
|
|
101
|
+
Fetch disk and memory utilization stats from the API.
|
|
102
|
+
:returns: An instance of `SystemStats` populated from the target device.
|
|
103
|
+
"""
|
|
104
|
+
system_stats_url = urllib.parse.urljoin(self._url, "/api/system-stats")
|
|
105
|
+
logging.info(f"Fetching system stats from: {system_stats_url}")
|
|
106
|
+
response = requests.get(system_stats_url)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
return SystemStats.from_dict(response.json())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class QmdlManifestEntry:
|
|
9
|
+
|
|
10
|
+
"""Metadata for a single QMDL capture file.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name (str): The name of the QMDL file.
|
|
14
|
+
start_time (str): The start time of this capture file.
|
|
15
|
+
last_message_time (str): The timestamp of the last message captured in this file.
|
|
16
|
+
qmdl_size_bytes (int): The total size in bytes of this QMDL file.
|
|
17
|
+
analysis_size_bytes (int): The total size in bytes of the analysis file associated with this QMDL capture file.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
start_time: str
|
|
23
|
+
last_message_time: str
|
|
24
|
+
qmdl_size_bytes: int
|
|
25
|
+
analysis_size_bytes: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class QmdlManifest:
|
|
30
|
+
|
|
31
|
+
"""A collection of metadata for all QMDL capture files available on this system.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
entries (List[QmdlManifestEntry]): A list of metadata for all finalized QMDL capture files available on this system.
|
|
35
|
+
current_entry (Optional[QmdlManifestEntry]): An optional value containing information on the active capture, or `None` if there is no active capture.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
entries: List[QmdlManifestEntry]
|
|
40
|
+
current_entry: Optional[QmdlManifestEntry]
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_dict(qmdl_manifest: dict):
|
|
44
|
+
qmdl_manifest["entries"] = [QmdlManifestEntry(**x) for x in qmdl_manifest["entries"]]
|
|
45
|
+
if qmdl_manifest["current_entry"] is not None:
|
|
46
|
+
qmdl_manifest["current_entry"] = QmdlManifestEntry(**qmdl_manifest["current_entry"])
|
|
47
|
+
return QmdlManifest(**qmdl_manifest)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _size_str_to_int(size: str) -> int:
|
|
5
|
+
"""
|
|
6
|
+
Convert string notation of megabytes (e.g. 214.7M) to an integer value representing bytes (e.g. 225129267).
|
|
7
|
+
:param size: String notation, megabytes (e.g. 214.7M)
|
|
8
|
+
:returns: An integer value representing bytes (e.g. 225129267)
|
|
9
|
+
"""
|
|
10
|
+
if size[-1] != "M":
|
|
11
|
+
raise ValueError(f"Unsupported size suffix: {size[-1]} ({size})")
|
|
12
|
+
return int(float(size.rstrip("M")) * 1048576)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DiskStats:
|
|
17
|
+
|
|
18
|
+
"""Disk usage statistics and information about the underlying filesystem, obtained from the Rayhunter API.
|
|
19
|
+
|
|
20
|
+
Size and percentage values are converted from string (e.g. 214.7M) to bytes (e.g. 225129267) for programmatic ease of use.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
partition (str): The partition Rayhunter is mounted on (e.g. ubi0:usrfs).
|
|
24
|
+
total_size (int): Total size of the disk in bytes (e.g. 225129267).
|
|
25
|
+
used_size (int): The amount of disk space currently in use in bytes (e.g. 18350080).
|
|
26
|
+
available_size (int): The amount of disk space currently available in bytes (e.g. 206884044).
|
|
27
|
+
used_percent (int): The percentage of disk space currently in use (e.g. 8).
|
|
28
|
+
mounted_on (str): Rayhunter's mount point (e.g. /data).
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
partition: str
|
|
33
|
+
total_size: int
|
|
34
|
+
used_size: int
|
|
35
|
+
available_size: int
|
|
36
|
+
used_percent: int
|
|
37
|
+
mounted_on: str
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def from_dict(disk_stats: dict):
|
|
41
|
+
for size_key in ["total_size", "used_size", "available_size"]:
|
|
42
|
+
disk_stats[size_key] = _size_str_to_int(disk_stats[size_key])
|
|
43
|
+
disk_stats["used_percent"] = int(disk_stats["used_percent"].rstrip("%"))
|
|
44
|
+
return DiskStats(**disk_stats)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class MemoryStats:
|
|
49
|
+
|
|
50
|
+
"""Memory usage statistics, obtained from the Rayhunter API.
|
|
51
|
+
|
|
52
|
+
Size and percentage values are converted from string (e.g. 159.9) to bytes (e.g. 167667302) for programmatic ease of use.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
total (int): Total size of memory in bytes (e.g. 167667302).
|
|
56
|
+
used (int): The amount of memory currently in use in bytes (e.g. 149212364).
|
|
57
|
+
free (int): The amount of memory currently available in bytes (e.g. 18454937)
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
total: int
|
|
62
|
+
used: int
|
|
63
|
+
free: int
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def from_dict(memory_stats):
|
|
67
|
+
for key in ["total", "used", "free"]:
|
|
68
|
+
memory_stats[key] = _size_str_to_int(memory_stats[key])
|
|
69
|
+
return MemoryStats(**memory_stats)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class SystemStats:
|
|
74
|
+
|
|
75
|
+
"""Disk and memory utilization statistics for the underlying system, pulled from the Rayhunter API.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
disk_stats (DiskStats): Information on the underlying disk
|
|
79
|
+
memory_stats (MemoryStats): Information on system memory utilization
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
disk_stats: DiskStats
|
|
84
|
+
memory_stats: MemoryStats
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def from_dict(system_stats: dict):
|
|
88
|
+
return SystemStats(
|
|
89
|
+
disk_stats=DiskStats.from_dict(system_stats["disk_stats"]),
|
|
90
|
+
memory_stats=MemoryStats.from_dict(system_stats["memory_stats"])
|
|
91
|
+
)
|