remoteRF-server-testing 0.0.0__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.
Files changed (44) hide show
  1. remoteRF_server/__init__.py +0 -0
  2. remoteRF_server/common/__init__.py +0 -0
  3. remoteRF_server/common/grpc/__init__.py +1 -0
  4. remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
  5. remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
  6. remoteRF_server/common/grpc/grpc_pb2.py +59 -0
  7. remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
  8. remoteRF_server/common/idl/__init__.py +1 -0
  9. remoteRF_server/common/idl/device_schema.py +39 -0
  10. remoteRF_server/common/idl/pluto_schema.py +174 -0
  11. remoteRF_server/common/idl/schema.py +358 -0
  12. remoteRF_server/common/utils/__init__.py +6 -0
  13. remoteRF_server/common/utils/ansi_codes.py +120 -0
  14. remoteRF_server/common/utils/api_token.py +21 -0
  15. remoteRF_server/common/utils/db_connection.py +35 -0
  16. remoteRF_server/common/utils/db_location.py +24 -0
  17. remoteRF_server/common/utils/list_string.py +5 -0
  18. remoteRF_server/common/utils/process_arg.py +80 -0
  19. remoteRF_server/drivers/__init__.py +0 -0
  20. remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  21. remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
  22. remoteRF_server/host/__init__.py +0 -0
  23. remoteRF_server/host/host_auth_token.py +292 -0
  24. remoteRF_server/host/host_directory_store.py +142 -0
  25. remoteRF_server/host/host_tunnel_server.py +1388 -0
  26. remoteRF_server/server/__init__.py +0 -0
  27. remoteRF_server/server/acc_perms.py +317 -0
  28. remoteRF_server/server/cert_provider.py +184 -0
  29. remoteRF_server/server/device_manager.py +688 -0
  30. remoteRF_server/server/grpc_server.py +1023 -0
  31. remoteRF_server/server/reservation.py +811 -0
  32. remoteRF_server/server/rpc_manager.py +104 -0
  33. remoteRF_server/server/user_group_cli.py +723 -0
  34. remoteRF_server/server/user_group_handler.py +1120 -0
  35. remoteRF_server/serverrf_cli.py +1377 -0
  36. remoteRF_server/tools/__init__.py +191 -0
  37. remoteRF_server/tools/gen_certs.py +274 -0
  38. remoteRF_server/tools/gist_status.py +139 -0
  39. remoteRF_server/tools/gist_status_testing.py +67 -0
  40. remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
  41. remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
  42. remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
  43. remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
  44. remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,317 @@
1
+ import sqlite3
2
+ import threading
3
+ from typing import Tuple
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ from functools import wraps
8
+
9
+ from ..common.utils import *
10
+ from .device_manager import get_all_devices
11
+ from .user_group_handler import user_group_handler
12
+
13
+ class userperms:
14
+ def __init__(self): # default user perms settings
15
+ self.id = 'user'
16
+ # self.max_reservations = 1
17
+ # self.max_reservation_time_sec = 1800 # half a hour
18
+ # self.devices_allowed = [0] # ex: only pluto
19
+
20
+ # can only cancel personal reservations, and can only view reservations on the devices they have access to
21
+
22
+ # def __str__(self):
23
+ # return f"Max Reservations: {self.max_reservations}\nMax Reservation Time: {int(self.max_reservation_time_sec/60)} minutes\nDevice IDs Accessible: {self.devices_allowed}"
24
+
25
+ class perms_db:
26
+ def __init__(self):
27
+ # self.filepath = Path(__file__).resolve().parent.parent/'db'/'perms.db'
28
+
29
+ self.filepath = get_db_dir() / 'perms.db'
30
+ self.db = sqlite3.connect(self.filepath)
31
+ self.cursor = self.db.cursor()
32
+
33
+ self.cursor.execute('''
34
+ CREATE TABLE IF NOT EXISTS Users (
35
+ UserID INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ Username TEXT NOT NULL UNIQUE
37
+ );
38
+ ''')
39
+
40
+ self.cursor.execute('''
41
+ CREATE TABLE IF NOT EXISTS PowerUsers (
42
+ UserID INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ Username TEXT NOT NULL UNIQUE,
44
+ MaxReservations INTEGER NOT NULL,
45
+ MaxReservationTimeSec INTEGER NOT NULL,
46
+ DevicesAllowed TEXT NOT NULL
47
+ );
48
+ ''')
49
+
50
+ self.cursor.execute('''
51
+ CREATE TABLE IF NOT EXISTS Admins (
52
+ AdminID INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ Username TEXT NOT NULL UNIQUE
54
+ );
55
+ ''')
56
+
57
+ self.db.commit()
58
+ self.cursor.close()
59
+ self.db.close()
60
+ self.userperms = userperms()
61
+ self.lock = threading.Lock()
62
+ self.min_reservation_time_sec = 600 # 10 minutes
63
+
64
+ @db_connection
65
+ def get_perms(self, username) -> Tuple[str, list[int], int, int]:
66
+ # SQL query to search for a user in all three tables
67
+ query = """
68
+ SELECT 'Normal User' as Type, UserID, Username, NULL as MaxReservations, NULL as MaxReservationTimeSec, NULL as DevicesAllowed FROM Users WHERE Username = ?
69
+ UNION ALL
70
+ SELECT 'Power User', UserID, Username, MaxReservations, MaxReservationTimeSec, DevicesAllowed FROM PowerUsers WHERE Username = ?
71
+ UNION ALL
72
+ SELECT 'Admin', AdminID as UserID, Username, NULL, NULL, NULL FROM Admins WHERE Username = ?
73
+ """
74
+
75
+ # Execute the query
76
+ self.cursor.execute(query, (username, username, username))
77
+
78
+ # Fetch all results
79
+ results = self.cursor.fetchall()
80
+
81
+ # print(f"Results: {results}")
82
+
83
+ # Return the results
84
+ return results
85
+
86
+ @db_connection
87
+ def get_normal_user_uid(self, username:str) -> int:
88
+ # SQL query to select the UserID from the Users table
89
+ query = 'SELECT UserID FROM Users WHERE Username = ?'
90
+
91
+ # Execute the query
92
+ self.cursor.execute(query, (username,))
93
+
94
+ # Fetch the result
95
+ result = self.cursor.fetchone()
96
+
97
+ # Return the UserID or -1 if not found
98
+ return result[0] if result else -1
99
+
100
+ def validate_reservation_request(self, accid, username, device_id, start_time, end_time, current_reservation_count:int, is_server=False, reserved_device_ids=[]) -> Tuple[bool, str]:
101
+ # check basic reservation requirements
102
+ if start_time > end_time:
103
+ return False, 'Start time must be before end time'
104
+ if end_time < datetime.now():
105
+ return False, 'End time must not be in the past'
106
+
107
+ # check if device exists
108
+ devices = get_all_devices()
109
+ if device_id not in devices:
110
+ return False, 'Invalid device ID. Device either does not exist or is disconnected.'
111
+
112
+ # check reservation duration
113
+ reservation_duration = (end_time - start_time).total_seconds()
114
+ if reservation_duration < 600:
115
+ return False, 'Minimum reservation time is 10 minutes.'
116
+
117
+ if is_server:
118
+ return True, ''
119
+
120
+ # Perm check
121
+ perms = self.get_perms(username)[0]
122
+
123
+ if perms[0] == 'Normal User':
124
+
125
+ uid = accid
126
+
127
+ allowed_res, max_res = user_group_handler.get_users_max_reservations(uid=uid, device_id=device_id)
128
+ allowed_time, max_time = user_group_handler.get_users_max_reservation_time(uid=uid, device_id=device_id)
129
+
130
+ if not allowed_res or not allowed_time:
131
+ return False, 'Invalid device ID. Device either does not exist or is disconnected.'
132
+
133
+ # check if user has reached max reservations
134
+ # if current_reservation_count >= max_res:
135
+ # return False, 'User has reached max reservations.'
136
+
137
+ # double dipping, and calculated PER GROUP (for max reservations)
138
+ allowed_res, group_used, group_max, _gid = user_group_handler.get_group_quota_for_device(
139
+ uid=uid,
140
+ device_id=device_id,
141
+ reserved_device_ids=reserved_device_ids,
142
+ )
143
+
144
+ if not allowed_res:
145
+ return False, 'Invalid device ID. Device either does not exist or is disconnected.'
146
+
147
+ if group_used > group_max:
148
+ return False, 'User has reached max reservations.'
149
+
150
+ # check if user has reached max reservation time
151
+ if reservation_duration > max_time:
152
+ return False, f'Max Reservation Duration allowed is {max_time/60} minutes.'
153
+
154
+ # # check if user has reached max reservations
155
+ # max_reservation_info = user_group_handler.get_users_max_reservations(uid=uid, device_id=device_id)
156
+ # if max_reservation_info[0] and current_reservation_count >= max_reservation_info[1]:
157
+ # return False, 'User has reached max reservations.'
158
+
159
+ # # check if user has reached max reservation time
160
+ # max_duration_time_info = user_group_handler.get_users_max_reservation_time(uid=uid, device_id=device_id)
161
+ # if max_duration_time_info[0] and reservation_duration > max_duration_time_info[1]:
162
+ # return False, f'Max Reservation Duration allowed is {max_duration_time_info[1]/60} minutes.'
163
+
164
+ # check if user has permission to reserve device
165
+ # if device_id not in self.userperms.devices_allowed:
166
+ # return False, 'Invalid device ID. Device either does not exist or is disconnected.'
167
+
168
+ # # check if user has reached max reservations
169
+ # if current_reservation_count >= self.userperms.max_reservations:
170
+ # return False, 'User has reached max reservations.'
171
+
172
+ # # check if user has reached max reservation time
173
+ # if reservation_duration > self.userperms.max_reservation_time_sec:
174
+ # return False, f'Max Reservation Duration allowed is {self.userperms.max_reservation_time_sec/60} minutes.'
175
+
176
+ elif perms[0] == 'Power User':
177
+ # check if user has permission to reserve device
178
+ if device_id not in str_to_list(perms[5]):
179
+ return False, 'Invalid device ID. Device either does not exist or is disconnected.'
180
+
181
+ # check if user has reached max reservations
182
+ if current_reservation_count >= int(perms[3]):
183
+ return False, f'Max Reservations allowed is {perms[3]}.'
184
+
185
+ # check if user has reached max reservation time
186
+ if reservation_duration > int(perms[4]):
187
+ return False, f'Max Reservation Duration allowed is {perms[4]/60} minutes.'
188
+
189
+ elif perms[0] == 'Admin':
190
+ pass # admin has no restrictions
191
+ else:
192
+ return False, 'Perms could not be found. Access Denied.'
193
+ return True, ''
194
+
195
+ @db_connection
196
+ def set_user(self, username):
197
+ # SQL command to insert a new user
198
+ insert_query = 'INSERT INTO Users (Username) VALUES (?)'
199
+
200
+ try:
201
+ # Execute the SQL command
202
+ self.remove_user(username)
203
+ self.cursor.execute(insert_query, (username,))
204
+
205
+ except sqlite3.Error as e:
206
+ print("An error occurred:", e)
207
+
208
+ @db_connection
209
+ def set_admin(self, username:str):
210
+ print(f"Setting {username} as an admin")
211
+ inpu = input(f"Are you sure you want to set {username} as an admin? \nThis gives them unrestricted access. (Y/N): ")
212
+ if inpu != 'Y' and inpu != 'y':
213
+ print(f"Admin set is canceled for user: {username}.")
214
+ return
215
+
216
+
217
+ # SQL command to insert a new user
218
+ insert_query = 'INSERT INTO Admins (Username) VALUES (?)'
219
+
220
+ try:
221
+ # Execute the SQL command
222
+ self.remove_user(username)
223
+ self.cursor.execute(insert_query, (username,))
224
+ print("Admin added successfully")
225
+
226
+ except sqlite3.Error as e:
227
+ print("An error occurred:", e)
228
+
229
+ @db_connection
230
+ def set_poweruser(self, username:str, max_reservations:int, max_reservation_time_sec:int, devices_allowed:list[int]):
231
+
232
+ print(f"Setting {username} as a PowerUser")
233
+
234
+ # SQL command to insert a new user
235
+ insert_query = 'INSERT INTO PowerUsers (Username, MaxReservations, MaxReservationTimeSec, DevicesAllowed) VALUES (?, ?, ?, ?)'
236
+
237
+ try:
238
+ # Execute the SQL command
239
+ self.remove_user(username)
240
+ self.cursor.execute(insert_query, (username, max_reservations, max_reservation_time_sec, list_to_str(devices_allowed)))
241
+ print(f"PowerUser perm for {username} added successfully")
242
+
243
+ except sqlite3.Error as e:
244
+ print("An error occurred:", e)
245
+ return
246
+
247
+
248
+ def remove_user(self, username:str) -> bool:
249
+ # List of tables to check
250
+ tables = ['Users', 'PowerUsers', 'Admins']
251
+
252
+ try:
253
+ # Attempt to delete the user from each table
254
+ for table in tables:
255
+ self.cursor.execute(f'DELETE FROM {table} WHERE Username = ?', (username,))
256
+
257
+ except sqlite3.Error as e:
258
+ print("An error occurred:", e)
259
+
260
+ @db_connection
261
+ def delete_user_from_reservation_handler(self, username:str):
262
+ self.remove_user(username)
263
+
264
+ @db_connection
265
+ def print_perms(self):
266
+ print("Printing all permissions:")
267
+ print("Users:")
268
+ # SQL query to select all users from the Users table
269
+ query = 'SELECT * FROM Users'
270
+
271
+ # Execute the query
272
+ self.cursor.execute(query)
273
+
274
+ # Fetch all results
275
+ results = self.cursor.fetchall()
276
+
277
+ # Print the results
278
+ for result in results:
279
+ print(result)
280
+
281
+ print("\nPowerUsers:")
282
+ # SQL query to select all users from the PowerUsers table
283
+ query = 'SELECT * FROM PowerUsers'
284
+
285
+ # Execute the query
286
+ self.cursor.execute(query)
287
+
288
+ # Fetch all results
289
+ results = self.cursor.fetchall()
290
+
291
+ # Print the results
292
+ for result in results:
293
+ print(result)
294
+
295
+ print("\nAdmins:")
296
+ # SQL query to select all users from the Admins table
297
+ query = 'SELECT * FROM Admins'
298
+
299
+ # Execute the query
300
+ self.cursor.execute(query)
301
+
302
+ # Fetch all results
303
+ results = self.cursor.fetchall()
304
+
305
+ # Print the results
306
+ for result in results:
307
+ print(result)
308
+
309
+ @db_connection
310
+ def rm_db(self):
311
+ # SQL command to delete all entries from all tables
312
+ query = 'DELETE FROM Users; DELETE FROM PowerUsers; DELETE FROM Admins'
313
+
314
+ # Execute the query
315
+ self.cursor.executescript(query)
316
+
317
+ perms = perms_db()
@@ -0,0 +1,184 @@
1
+ # cert_provider.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import socketserver
8
+ import threading
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Optional, Tuple
12
+
13
+ CERT_NAME = "server.crt"
14
+
15
+ def resolve_ca_path(ca_path: Optional[str | Path] = None) -> Path:
16
+ """
17
+ Resolve the CA certificate path.
18
+
19
+ Priority:
20
+ 1) explicit ca_path
21
+ 2) $REMOTERF_CERT_DIR/ca.crt
22
+ 3) $XDG_CONFIG_HOME/remoterf/certs/ca.crt (or ~/.config/...)
23
+ 4) fallback to repo-relative candidates (for dev)
24
+ """
25
+ if ca_path is not None:
26
+ p = Path(ca_path).expanduser().resolve()
27
+ if not p.exists():
28
+ raise FileNotFoundError(f"CA cert not found at: {p}")
29
+ return p
30
+
31
+ # --- preferred: user config location ---
32
+ xdg_config = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
33
+ cert_dir = Path(os.getenv("REMOTERF_CERT_DIR", xdg_config / "remoterf" / "certs"))
34
+ p = (cert_dir / CERT_NAME).expanduser().resolve()
35
+ if p.exists():
36
+ return p
37
+
38
+ # --- fallback: repo-relative for local dev ---
39
+ here = Path(__file__).resolve()
40
+ candidates = [
41
+ here.parent / "certs" / CERT_NAME,
42
+ here.parent.parent / "core" / "certs" / CERT_NAME,
43
+ here.parent.parent / "certs" / CERT_NAME,
44
+ ]
45
+ for c in candidates:
46
+ if c.exists():
47
+ return c
48
+
49
+ raise FileNotFoundError(
50
+ f"Could not find {CERT_NAME}. Tried:\n "
51
+ + "\n ".join([str(p)] + [str(c) for c in candidates])
52
+ )
53
+
54
+ def load_ca_pem(ca_path: Optional[str | Path] = None) -> bytes:
55
+ p = resolve_ca_path(ca_path)
56
+ data = p.read_bytes()
57
+ if b"BEGIN CERTIFICATE" not in data:
58
+ raise ValueError(f"{p} does not look like a PEM certificate.")
59
+ return data
60
+
61
+ def sha256_fingerprint_pem(pem_bytes: bytes) -> str:
62
+ h = hashlib.sha256(pem_bytes).hexdigest()
63
+ return ":".join(h[i:i+2] for i in range(0, len(h), 2))
64
+
65
+ # Server implementation
66
+
67
+ class _CertProviderHandler(socketserver.BaseRequestHandler):
68
+ def handle(self) -> None:
69
+ try:
70
+ self.request.settimeout(1.0)
71
+ first = b""
72
+ try:
73
+ first = self.request.recv(4096)
74
+ except Exception:
75
+ first = b""
76
+
77
+ pem = self.server.ca_pem # type: ignore[attr-defined]
78
+
79
+ if first.startswith(b"GET "):
80
+ self._handle_http(first, pem)
81
+ else:
82
+ self._handle_raw(pem)
83
+ except Exception:
84
+ return
85
+
86
+ def _handle_raw(self, pem: bytes) -> None:
87
+ self.request.sendall(pem)
88
+
89
+ def _handle_http(self, first: bytes, pem: bytes) -> None:
90
+ # Drain headers best-effort
91
+ try:
92
+ self.request.settimeout(0.2)
93
+ while b"\r\n\r\n" not in first and len(first) < 65536:
94
+ chunk = self.request.recv(4096)
95
+ if not chunk:
96
+ break
97
+ first += chunk
98
+ except Exception:
99
+ pass
100
+
101
+ # Parse request line
102
+ try:
103
+ line = first.split(b"\r\n", 1)[0].decode("utf-8", "replace")
104
+ except Exception:
105
+ line = "GET / HTTP/1.1"
106
+
107
+ parts = line.split()
108
+ path = parts[1] if len(parts) >= 2 else "/"
109
+
110
+ if path not in ("/", f"/{CERT_NAME}", "/ca.pem", "/ca.crt"):
111
+ body = b"Not Found\n"
112
+ resp = (
113
+ b"HTTP/1.1 404 Not Found\r\n"
114
+ b"Content-Type: text/plain\r\n"
115
+ + f"Content-Length: {len(body)}\r\n".encode("ascii")
116
+ + b"\r\n"
117
+ + body
118
+ )
119
+ self.request.sendall(resp)
120
+ return
121
+
122
+ body = pem
123
+ resp = (
124
+ b"HTTP/1.1 200 OK\r\n"
125
+ b"Content-Type: application/x-pem-file\r\n"
126
+ + f"Content-Length: {len(body)}\r\n".encode("ascii")
127
+ + b"Cache-Control: no-store\r\n"
128
+ + b"\r\n"
129
+ + body
130
+ )
131
+ self.request.sendall(resp)
132
+
133
+
134
+ class _ThreadingCertServer(socketserver.ThreadingTCPServer):
135
+ allow_reuse_address = True
136
+
137
+ def __init__(self, server_address: Tuple[str, int], ca_pem: bytes):
138
+ super().__init__(server_address, _CertProviderHandler)
139
+ self.ca_pem = ca_pem
140
+
141
+
142
+ def start_cert_provider(
143
+ *,
144
+ host: str,
145
+ port: int,
146
+ ca_path: Optional[str | Path] = None,
147
+ daemon: bool = True,
148
+ ) -> Tuple[_ThreadingCertServer, threading.Thread]:
149
+ ca_pem = load_ca_pem(ca_path)
150
+ server = _ThreadingCertServer((host, port), ca_pem=ca_pem)
151
+
152
+ t = threading.Thread(target=server.serve_forever, daemon=daemon)
153
+ t.start()
154
+ return server, t
155
+
156
+ def stop_cert_provider(server: _ThreadingCertServer) -> None:
157
+ server.shutdown()
158
+ server.server_close()
159
+
160
+ # Optional standalone mode
161
+
162
+ def main() -> None:
163
+ parser = argparse.ArgumentParser(description="RemoteRF CA certificate bootstrap provider.")
164
+ parser.add_argument("--host", required=True)
165
+ parser.add_argument("--port", type=int, required=True)
166
+ parser.add_argument("--ca", type=str, default=None, help="Path to ca.crt (PEM).")
167
+ args = parser.parse_args()
168
+
169
+ ca_pem = load_ca_pem(args.ca)
170
+ fp = sha256_fingerprint_pem(ca_pem)
171
+ print(f"[cert_provider] Listening on {args.host}:{args.port}")
172
+ print(f"[cert_provider] CA SHA256 fingerprint: {fp}")
173
+ print(f"[cert_provider] CA path: {resolve_ca_path(args.ca)}")
174
+
175
+ server = _ThreadingCertServer((args.host, args.port), ca_pem=ca_pem)
176
+ try:
177
+ server.serve_forever()
178
+ except KeyboardInterrupt:
179
+ pass
180
+ finally:
181
+ stop_cert_provider(server)
182
+
183
+ if __name__ == "__main__":
184
+ main()