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.
- remoteRF_server/__init__.py +0 -0
- remoteRF_server/common/__init__.py +0 -0
- remoteRF_server/common/grpc/__init__.py +1 -0
- remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
- remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
- remoteRF_server/common/grpc/grpc_pb2.py +59 -0
- remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
- remoteRF_server/common/idl/__init__.py +1 -0
- remoteRF_server/common/idl/device_schema.py +39 -0
- remoteRF_server/common/idl/pluto_schema.py +174 -0
- remoteRF_server/common/idl/schema.py +358 -0
- remoteRF_server/common/utils/__init__.py +6 -0
- remoteRF_server/common/utils/ansi_codes.py +120 -0
- remoteRF_server/common/utils/api_token.py +21 -0
- remoteRF_server/common/utils/db_connection.py +35 -0
- remoteRF_server/common/utils/db_location.py +24 -0
- remoteRF_server/common/utils/list_string.py +5 -0
- remoteRF_server/common/utils/process_arg.py +80 -0
- remoteRF_server/drivers/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
- remoteRF_server/host/__init__.py +0 -0
- remoteRF_server/host/host_auth_token.py +292 -0
- remoteRF_server/host/host_directory_store.py +142 -0
- remoteRF_server/host/host_tunnel_server.py +1388 -0
- remoteRF_server/server/__init__.py +0 -0
- remoteRF_server/server/acc_perms.py +317 -0
- remoteRF_server/server/cert_provider.py +184 -0
- remoteRF_server/server/device_manager.py +688 -0
- remoteRF_server/server/grpc_server.py +1023 -0
- remoteRF_server/server/reservation.py +811 -0
- remoteRF_server/server/rpc_manager.py +104 -0
- remoteRF_server/server/user_group_cli.py +723 -0
- remoteRF_server/server/user_group_handler.py +1120 -0
- remoteRF_server/serverrf_cli.py +1377 -0
- remoteRF_server/tools/__init__.py +191 -0
- remoteRF_server/tools/gen_certs.py +274 -0
- remoteRF_server/tools/gist_status.py +139 -0
- remoteRF_server/tools/gist_status_testing.py +67 -0
- remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
- remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
- remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
- remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
- 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()
|