python-rayhunter 2025.3.1__py3-none-any.whl

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.
@@ -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,9 @@
1
+ python_rayhunter-2025.3.1.dist-info/licenses/LICENSE,sha256=4LHwVtwZNgbaRLzi-IQFBGpI-BwfCzPE-O7uJQWtALE,1061
2
+ rayhunter/__init__.py,sha256=HF1EAkB8DJO-TarR9n4s5lLyPu8_o-r0d7EEXNRJpuI,64
3
+ rayhunter/api.py,sha256=EJW3HR_2wRns86IIUKEI-h3BlvJMNyTojl3UsxJuliI,4618
4
+ rayhunter/manifest.py,sha256=V_U7-mdcaDRxGvysLQDdkylOl85hwmURbKXYTVST0aU,1575
5
+ rayhunter/system_stats.py,sha256=DXc27bJaBceH74FjqS2-lXLov3yBUs8eSsEfB9bXboQ,3081
6
+ python_rayhunter-2025.3.1.dist-info/METADATA,sha256=fmFj2lYrK-oxPTjRQ1TEYKTTTfMFTABz1SZQQmFIuYQ,5255
7
+ python_rayhunter-2025.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ python_rayhunter-2025.3.1.dist-info/top_level.txt,sha256=GGniULEc2vexd1qBwVhxi7nUx9ONWigBRt8DJ6vFddY,10
9
+ python_rayhunter-2025.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ rayhunter
rayhunter/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .api import RayhunterApi
2
+
3
+
4
+ __all__ = [
5
+ "RayHunterApi"
6
+ ]
rayhunter/api.py ADDED
@@ -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())
rayhunter/manifest.py ADDED
@@ -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
+ )