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
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from functools import wraps
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from ..common.utils import *
|
|
13
|
+
from .device_manager import get_all_devices, set_device, get_all_devices_str, start_transmitter, terminate_transmitter, get_transmitter_state
|
|
14
|
+
from .acc_perms import perms, userperms
|
|
15
|
+
from .user_group_handler import user_group_handler
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
allow_connections = True
|
|
22
|
+
|
|
23
|
+
class ReservationHandler:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.filepath = get_db_dir() / "reservations.db"
|
|
26
|
+
# self.filepath = Path(__file__).resolve().parent.parent/'db'/'reservations.db'
|
|
27
|
+
|
|
28
|
+
self.db = sqlite3.connect(self.filepath)
|
|
29
|
+
self.cursor = self.db.cursor()
|
|
30
|
+
|
|
31
|
+
self.cursor.execute('''
|
|
32
|
+
CREATE TABLE IF NOT EXISTS Accounts (
|
|
33
|
+
acc_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
username TEXT NOT NULL UNIQUE,
|
|
35
|
+
email TEXT NOT NULL UNIQUE,
|
|
36
|
+
pass_salt TEXT NOT NULL,
|
|
37
|
+
pass_hash TEXT NOT NULL,
|
|
38
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
39
|
+
)
|
|
40
|
+
''')
|
|
41
|
+
|
|
42
|
+
self.cursor.execute('''
|
|
43
|
+
CREATE TABLE IF NOT EXISTS Reservations (
|
|
44
|
+
res_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
username TEXT NOT NULL,
|
|
46
|
+
device_id INTEGER NOT NULL,
|
|
47
|
+
start_time TIMESTAMP NOT NULL,
|
|
48
|
+
end_time TIMESTAMP NOT NULL,
|
|
49
|
+
salt TEXT NOT NULL,
|
|
50
|
+
hash TEXT NOT NULL
|
|
51
|
+
)
|
|
52
|
+
''')
|
|
53
|
+
|
|
54
|
+
self.cursor.execute('''
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_device_id ON Reservations(device_id);
|
|
56
|
+
''')
|
|
57
|
+
|
|
58
|
+
self.cursor.close()
|
|
59
|
+
self.db.commit()
|
|
60
|
+
self.db.close()
|
|
61
|
+
self.lock = threading.RLock()
|
|
62
|
+
|
|
63
|
+
def handle_call(self, *, function_name, args):
|
|
64
|
+
if not allow_connections:
|
|
65
|
+
return {"ACC": map_arg("Server is not accepting connections at the moment. Please try later.")}
|
|
66
|
+
try:
|
|
67
|
+
if function_name == 'create_user':
|
|
68
|
+
return self.create_user(**args)
|
|
69
|
+
elif function_name == 'reserve_device':
|
|
70
|
+
username = unmap_arg(args['un'])
|
|
71
|
+
password = unmap_arg(args['pw'])
|
|
72
|
+
if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
73
|
+
starttime = datetime.fromtimestamp(unmap_arg(args['st']))
|
|
74
|
+
endtime = datetime.fromtimestamp(unmap_arg(args['et']))
|
|
75
|
+
device_id = unmap_arg(args['dd'])
|
|
76
|
+
response = self.reserve_device(username, device_id, starttime, endtime)
|
|
77
|
+
self.update_devices()
|
|
78
|
+
return response
|
|
79
|
+
else:
|
|
80
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
81
|
+
elif function_name == 'get_res':
|
|
82
|
+
username = unmap_arg(args['un'])
|
|
83
|
+
password = unmap_arg(args['pw'])
|
|
84
|
+
if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
85
|
+
return self.grab_all_reservations(username)
|
|
86
|
+
else:
|
|
87
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
88
|
+
elif function_name == 'get_dev':
|
|
89
|
+
username = unmap_arg(args['un'])
|
|
90
|
+
password = unmap_arg(args['pw'])
|
|
91
|
+
if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
92
|
+
return self.grab_all_devices(username)
|
|
93
|
+
else:
|
|
94
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
95
|
+
elif function_name == 'login':
|
|
96
|
+
return self.login_user(unmap_arg(args['un']), unmap_arg(args['pw']))
|
|
97
|
+
elif function_name == 'cancel_res':
|
|
98
|
+
username = unmap_arg(args['un'])
|
|
99
|
+
password = unmap_arg(args['pw'])
|
|
100
|
+
if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
101
|
+
if self.validate_cancel_reservation(unmap_arg(args['res_id']), username):
|
|
102
|
+
return self.remove_reservation(res_id=unmap_arg(args['res_id']))
|
|
103
|
+
else:
|
|
104
|
+
return {"ace": map_arg("Invalid reservation ID or user does not own reservation.")}
|
|
105
|
+
else:
|
|
106
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
107
|
+
# elif function_name == 'get_perms':
|
|
108
|
+
# username = unmap_arg(args['un'])
|
|
109
|
+
# password = unmap_arg(args['pw'])
|
|
110
|
+
# if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
111
|
+
# data = perms.get_perms(username)
|
|
112
|
+
# if data[0][0] == 'Normal User':
|
|
113
|
+
# return {'UC': map_arg(str(data)), 'details': map_arg(str(perms.userperms))}
|
|
114
|
+
# return {'UC': map_arg(str(data))}
|
|
115
|
+
# else:
|
|
116
|
+
# return {"ace": map_arg("Invalid login credentials.")}
|
|
117
|
+
|
|
118
|
+
elif function_name == "get_perms":
|
|
119
|
+
username = unmap_arg(args["un"])
|
|
120
|
+
password = unmap_arg(args["pw"])
|
|
121
|
+
|
|
122
|
+
if self.login_user(username, password) == {"UC": map_arg(f"{username}")}:
|
|
123
|
+
data = perms.get_perms(username)
|
|
124
|
+
|
|
125
|
+
if data and data[0] and data[0][0] == "Normal User":
|
|
126
|
+
uid = int(self.get_uid(username))
|
|
127
|
+
|
|
128
|
+
# Force pure ints (defensive)
|
|
129
|
+
devs_raw = user_group_handler.get_users_devices(uid=uid) or []
|
|
130
|
+
devs: list[int] = []
|
|
131
|
+
for d in devs_raw:
|
|
132
|
+
try:
|
|
133
|
+
devs.append(int(d))
|
|
134
|
+
except Exception:
|
|
135
|
+
continue
|
|
136
|
+
devs = sorted(set(devs))
|
|
137
|
+
|
|
138
|
+
# caps: force string keys for JSON + client lookups
|
|
139
|
+
caps: dict[str, dict] = {}
|
|
140
|
+
for d in devs:
|
|
141
|
+
_allowed_t, max_t = user_group_handler.get_users_max_reservation_time(uid=uid, device_id=d)
|
|
142
|
+
_allowed_r, max_r = user_group_handler.get_users_max_reservations(uid=uid, device_id=d)
|
|
143
|
+
caps[str(d)] = {
|
|
144
|
+
"max_reservation_time_sec": int(max_t),
|
|
145
|
+
"max_reservations": int(max_r),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
groups_raw = user_group_handler.get_groups_user_in(uid=uid) or []
|
|
149
|
+
groups: list[str] = []
|
|
150
|
+
for g in groups_raw:
|
|
151
|
+
if g is None:
|
|
152
|
+
continue
|
|
153
|
+
s = str(g).strip()
|
|
154
|
+
if s:
|
|
155
|
+
groups.append(s)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"UC": map_arg(str(data)),
|
|
159
|
+
"details": map_arg(json.dumps({
|
|
160
|
+
"devices": devs,
|
|
161
|
+
"caps": caps,
|
|
162
|
+
"groups": groups,
|
|
163
|
+
})),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {"UC": map_arg(str(data))}
|
|
167
|
+
|
|
168
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
elif function_name == 'set_enroll':
|
|
173
|
+
username = unmap_arg(args['un'])
|
|
174
|
+
password = unmap_arg(args['pw'])
|
|
175
|
+
if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
|
|
176
|
+
code_name = unmap_arg(args['ec'])
|
|
177
|
+
acc_id = self.get_uid(username)
|
|
178
|
+
if user_group_handler.enroll_user_with_code(uid=acc_id, code_name=code_name):
|
|
179
|
+
return {'UC': map_arg(f'Enrolled {username} with code {code_name}')}
|
|
180
|
+
else:
|
|
181
|
+
return {'UE': map_arg(f'Failed to enroll {username} with code {code_name}. Invalid or expired code.')}
|
|
182
|
+
else:
|
|
183
|
+
return {"ace": map_arg("Invalid login credentials.")}
|
|
184
|
+
return {"ace": map_arg(f"No matching server function to call: {function_name}")}
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return {"RPC Acc Error", map_arg(f'{e}')}
|
|
187
|
+
|
|
188
|
+
@db_connection
|
|
189
|
+
def get_uid(self, username:str) -> int:
|
|
190
|
+
self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
|
|
191
|
+
row = self.cursor.fetchone()
|
|
192
|
+
uid = int(row[0]) if row else -1
|
|
193
|
+
return uid
|
|
194
|
+
|
|
195
|
+
@db_connection
|
|
196
|
+
def create_user(self,*, un, pw, em, ec, ad:bool=False):
|
|
197
|
+
un = unmap_arg(un)
|
|
198
|
+
pw = unmap_arg(pw)
|
|
199
|
+
em = unmap_arg(em)
|
|
200
|
+
ec = unmap_arg(ec)
|
|
201
|
+
|
|
202
|
+
# check if username and password fit the bill
|
|
203
|
+
if len(un) < 3:
|
|
204
|
+
return {"UE": map_arg("Username must be at least 3 characters long.")}
|
|
205
|
+
if len(pw) < 5:
|
|
206
|
+
return {"UE": map_arg("Password must be at least 5 characters long.")}
|
|
207
|
+
if '@' not in em:
|
|
208
|
+
return {"UE": map_arg("Invalid email address.")}
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
if not user_group_handler.validate_enrollment_code(code_name=ec):
|
|
212
|
+
return {"UE": map_arg("Invalid or expired enrollment code.")}
|
|
213
|
+
except Exception:
|
|
214
|
+
return {"UE": map_arg("Invalid or expired enrollment code.")}
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
passwords = hash_token(pw)
|
|
218
|
+
self.cursor.execute('''
|
|
219
|
+
INSERT INTO Accounts (username, email, pass_salt, pass_hash, created_at) VALUES (?, ?, ?, ?, ?)
|
|
220
|
+
''', (un, em, passwords[0], passwords[1], datetime.now()))
|
|
221
|
+
uid = int(self.cursor.lastrowid)
|
|
222
|
+
user_group_handler.enroll_user_with_code(uid=uid, code_name=ec)
|
|
223
|
+
perms.set_user(un) # set perms
|
|
224
|
+
return {"UC": map_arg(f'{un}')}
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return {"UE": map_arg(f'{e}')}
|
|
227
|
+
|
|
228
|
+
@db_connection
|
|
229
|
+
def remove_user(self, *, username):
|
|
230
|
+
self.cursor.execute('DELETE FROM Accounts WHERE username = ?', (username,))
|
|
231
|
+
perms.delete_user_from_reservation_handler(username)
|
|
232
|
+
return {"UC": map_arg(f'Removed {username}')}
|
|
233
|
+
|
|
234
|
+
@db_connection
|
|
235
|
+
def validate_cancel_reservation(self, res_id:int, username:str) -> bool:
|
|
236
|
+
self.cursor.execute('SELECT * FROM Reservations WHERE res_id = ?', (res_id,))
|
|
237
|
+
reservation = self.cursor.fetchone()
|
|
238
|
+
if reservation is not None and reservation[1] == username:
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
@db_connection
|
|
243
|
+
def remove_reservation(self, *, res_id):
|
|
244
|
+
self.cursor.execute('DELETE FROM Reservations WHERE res_id = ?', (res_id,))
|
|
245
|
+
return {"UC": map_arg(f'Removed reservation {res_id}')}
|
|
246
|
+
|
|
247
|
+
@db_connection
|
|
248
|
+
def rm_db(self):
|
|
249
|
+
if input("Drop Tables? (y/n): ") == 'y' and input("Are you sure? This deletes ALL data stored on the server. (y/n): ") == 'y':
|
|
250
|
+
time.sleep(1)
|
|
251
|
+
if input("Last chance. Are you sure? (y/n): ") == 'y':
|
|
252
|
+
self.cursor.execute('DROP TABLE IF EXISTS Accounts')
|
|
253
|
+
self.cursor.execute('DROP TABLE IF EXISTS Reservations')
|
|
254
|
+
perms.rm_db()
|
|
255
|
+
sys.exit()
|
|
256
|
+
|
|
257
|
+
@db_connection
|
|
258
|
+
def remove_all_users(self):
|
|
259
|
+
self.cursor.execute('DELETE FROM Accounts')
|
|
260
|
+
print("All accounts removed.")
|
|
261
|
+
|
|
262
|
+
@db_connection
|
|
263
|
+
def remove_all_reservations(self):
|
|
264
|
+
self.cursor.execute('DELETE FROM Reservations')
|
|
265
|
+
print("All reservations removed.")
|
|
266
|
+
|
|
267
|
+
@db_connection
|
|
268
|
+
def login_user(self, username, password):
|
|
269
|
+
self.cursor.execute('SELECT * FROM Accounts WHERE username = ?', (username,))
|
|
270
|
+
account = self.cursor.fetchone()
|
|
271
|
+
|
|
272
|
+
if account is not None and validate_token(account[3], account[4], password):
|
|
273
|
+
return {"UC": map_arg(f"{username}")}
|
|
274
|
+
else:
|
|
275
|
+
return {"UE": map_arg("Invalid login details.")}
|
|
276
|
+
|
|
277
|
+
def _clean_expired_reservations(self, device_id: int):
|
|
278
|
+
"""Remove expired reservations from the database."""
|
|
279
|
+
current_time = datetime.now()
|
|
280
|
+
cur = self.db.cursor()
|
|
281
|
+
try:
|
|
282
|
+
cur.execute('DELETE FROM Reservations WHERE device_id = ? AND end_time < ?', (device_id, current_time))
|
|
283
|
+
finally:
|
|
284
|
+
cur.close()
|
|
285
|
+
|
|
286
|
+
@db_connection
|
|
287
|
+
def get_reserved_device_ids_for_user(self, username: str) -> list[int]:
|
|
288
|
+
"""
|
|
289
|
+
Returns one device_id per reservation row that is still active/upcoming.
|
|
290
|
+
Duplicates are intentional (counts reservations, not unique devices).
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
username = str(username).strip()
|
|
294
|
+
if not username:
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
now = datetime.now()
|
|
298
|
+
self.cursor.execute(
|
|
299
|
+
"""
|
|
300
|
+
SELECT device_id
|
|
301
|
+
FROM Reservations
|
|
302
|
+
WHERE username = ?
|
|
303
|
+
AND end_time > ?
|
|
304
|
+
""",
|
|
305
|
+
(username, now),
|
|
306
|
+
)
|
|
307
|
+
rows = self.cursor.fetchall() or []
|
|
308
|
+
|
|
309
|
+
out: list[int] = []
|
|
310
|
+
for (d,) in rows:
|
|
311
|
+
try:
|
|
312
|
+
out.append(int(d))
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
return out
|
|
316
|
+
except Exception:
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
@db_connection
|
|
320
|
+
def reserve_device(self, username, device_id: int, start_time, end_time, is_server=False):
|
|
321
|
+
|
|
322
|
+
self._clean_expired_reservations(device_id)
|
|
323
|
+
|
|
324
|
+
self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
|
|
325
|
+
row = self.cursor.fetchone()
|
|
326
|
+
accid = int(row[0]) if row else -1
|
|
327
|
+
|
|
328
|
+
# Check if reservation is valid with perms.
|
|
329
|
+
reserved_device_ids = self.get_reserved_device_ids_for_user(username)
|
|
330
|
+
reserved_device_ids = reserved_device_ids + [int(device_id)]
|
|
331
|
+
|
|
332
|
+
validation = perms.validate_reservation_request(
|
|
333
|
+
accid,
|
|
334
|
+
username,
|
|
335
|
+
device_id,
|
|
336
|
+
start_time,
|
|
337
|
+
end_time,
|
|
338
|
+
self.get_reservation_count(username),
|
|
339
|
+
is_server=is_server,
|
|
340
|
+
reserved_device_ids=reserved_device_ids,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if validation[0] == False:
|
|
344
|
+
return {"ace": map_arg(validation[1])}
|
|
345
|
+
|
|
346
|
+
# Check if the time slot is already reserved
|
|
347
|
+
self.cursor.execute('SELECT * FROM Reservations WHERE device_id = ? AND start_time < ? AND end_time > ?', (device_id, end_time, start_time))
|
|
348
|
+
|
|
349
|
+
if self.cursor.fetchone():
|
|
350
|
+
# Time slot is already reserved
|
|
351
|
+
return {"ace": map_arg("Device already reserved at this time. Please select another time or device.")}
|
|
352
|
+
|
|
353
|
+
token = generate_token()
|
|
354
|
+
|
|
355
|
+
# Store the reservation and hashed API token in the database
|
|
356
|
+
self.cursor.execute('INSERT INTO Reservations (username, device_id, start_time, end_time, salt, hash) VALUES (?, ?, ?, ?, ?, ?)',
|
|
357
|
+
(username, device_id, start_time, end_time, token[0], token[1]))
|
|
358
|
+
return {"Token": map_arg(token[2])}
|
|
359
|
+
|
|
360
|
+
@db_connection
|
|
361
|
+
def validate_token(self, *, token, device_id):
|
|
362
|
+
|
|
363
|
+
# check if the token is valid for the given device_id
|
|
364
|
+
# return 'Valid' if token is valid for the current time stamp (kick any users using atm)
|
|
365
|
+
# return 'Unocc' if no one is using the device at the current time stamp (past 5 minutes)
|
|
366
|
+
# return 'Invalid' o.w.
|
|
367
|
+
|
|
368
|
+
# TODO: Set for const lookup time for the api token? and perhaps check every minute to see if the token is still valid and then clean out of the list. HAVE the cleaning be on a separate thread.
|
|
369
|
+
|
|
370
|
+
# TODO: Store the token on the device itself in device manager, and then when the device is being used, it checks itself. Once the time is over, then tokens get replaced and cleaned out on the devices, and so subsquent calls cannot go through.
|
|
371
|
+
|
|
372
|
+
now = datetime.datetime.now()
|
|
373
|
+
cursor = self.db.cursor()
|
|
374
|
+
|
|
375
|
+
# Get the reservation details from the database
|
|
376
|
+
cursor.execute("""
|
|
377
|
+
SELECT * FROM Reservations WHERE start_time < ? AND end_time > ?
|
|
378
|
+
""", (device_id, now, now))
|
|
379
|
+
|
|
380
|
+
reservations = cursor.fetchone()
|
|
381
|
+
cursor.close()
|
|
382
|
+
|
|
383
|
+
if reservations is not None and validate_token(reservations[4], reservations[5], token):
|
|
384
|
+
return "Valid"
|
|
385
|
+
elif reservations is None:
|
|
386
|
+
return "Unocc"
|
|
387
|
+
else:
|
|
388
|
+
return "Invalid"
|
|
389
|
+
|
|
390
|
+
@db_connection
|
|
391
|
+
def grab_all_reservations_server(self):
|
|
392
|
+
self.cursor.execute('SELECT * FROM Reservations')
|
|
393
|
+
reservations = self.cursor.fetchall()
|
|
394
|
+
result = {}
|
|
395
|
+
for res in reservations:
|
|
396
|
+
result[int(res[0])] = res[1:-2]
|
|
397
|
+
return result
|
|
398
|
+
|
|
399
|
+
@db_connection
|
|
400
|
+
def grab_all_reservations(self, username:str, current_time=datetime.now()):
|
|
401
|
+
self.cursor.execute('SELECT * FROM Reservations')
|
|
402
|
+
reservations = self.cursor.fetchall()
|
|
403
|
+
perm = perms.get_perms(username)[0]
|
|
404
|
+
result = {}
|
|
405
|
+
if perm[0] == 'Admin':
|
|
406
|
+
for i, res in enumerate(reservations, start=1):
|
|
407
|
+
result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
|
|
408
|
+
|
|
409
|
+
# elif perm[0] == 'Normal User':
|
|
410
|
+
# normalperms = perms.userperms.devices_allowed
|
|
411
|
+
# for i, res in enumerate(reservations, start=1):
|
|
412
|
+
# if res[2] in normalperms:
|
|
413
|
+
# result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
|
|
414
|
+
|
|
415
|
+
elif perm[0] == 'Normal User':
|
|
416
|
+
self.cursor.execute("SELECT acc_id FROM Accounts WHERE username = ? LIMIT 1", (username,))
|
|
417
|
+
row = self.cursor.fetchone()
|
|
418
|
+
uid = int(row[0]) if row else -1
|
|
419
|
+
allowed = set(user_group_handler.get_users_devices(uid=uid))
|
|
420
|
+
|
|
421
|
+
for i, res in enumerate(reservations, start=1):
|
|
422
|
+
if int(res[2]) in allowed:
|
|
423
|
+
result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
elif perm[0] == 'Power User':
|
|
427
|
+
for i, res in enumerate(reservations, start=1):
|
|
428
|
+
if res[2] in str_to_list(perm[5]):
|
|
429
|
+
result[f'{res[0]}'] = map_arg(','.join(map(str, res[1:-2])))
|
|
430
|
+
|
|
431
|
+
return result
|
|
432
|
+
|
|
433
|
+
def grab_all_devices(self, username:str):
|
|
434
|
+
devices = get_all_devices_str()
|
|
435
|
+
perm = perms.get_perms(username)[0]
|
|
436
|
+
if perm[0] == 'Admin':
|
|
437
|
+
return {str(k): map_arg(str(v)) for k, v in devices.items()}
|
|
438
|
+
|
|
439
|
+
# if perm[0] == 'Normal User':
|
|
440
|
+
# normalperms = perms.userperms.devices_allowed
|
|
441
|
+
# return {str(k): map_arg(str(v)) for k, v in devices.items() if k in normalperms}
|
|
442
|
+
|
|
443
|
+
if perm[0] == 'Normal User':
|
|
444
|
+
uid = self.get_uid(username)
|
|
445
|
+
allowed = set(user_group_handler.get_users_devices(uid=uid))
|
|
446
|
+
return {str(k): map_arg(str(v)) for k, v in devices.items() if k in allowed}
|
|
447
|
+
|
|
448
|
+
if perm[0] == 'Power User':
|
|
449
|
+
return {str(k): map_arg(str(v)) for k, v in devices.items() if k in str_to_list(perm[5])}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@db_connection
|
|
453
|
+
def print_all_reservations(self):
|
|
454
|
+
print("Printing all reservations...")
|
|
455
|
+
cursor = self.db.cursor()
|
|
456
|
+
cursor.execute("""SELECT * FROM Reservations""")
|
|
457
|
+
reservations = cursor.fetchall()
|
|
458
|
+
cursor.close()
|
|
459
|
+
if reservations is not None:
|
|
460
|
+
for reservation in reservations:
|
|
461
|
+
print("Id:", reservation[0],"Username:", reservation[1], " Device ID:", reservation[2], " Start Time:", reservation[3], " End Time:", reservation[4])
|
|
462
|
+
else:
|
|
463
|
+
print("No reservations found.")
|
|
464
|
+
|
|
465
|
+
@db_connection
|
|
466
|
+
def get_all_accounts(self):
|
|
467
|
+
cursor = self.db.cursor()
|
|
468
|
+
cursor.execute("SELECT * FROM Accounts")
|
|
469
|
+
accounts = cursor.fetchall()
|
|
470
|
+
cursor.close()
|
|
471
|
+
return accounts
|
|
472
|
+
|
|
473
|
+
@db_connection
|
|
474
|
+
def print_all_accounts(self):
|
|
475
|
+
print("Printing all accounts...")
|
|
476
|
+
cursor = self.db.cursor()
|
|
477
|
+
cursor.execute("SELECT * FROM Accounts")
|
|
478
|
+
accounts = cursor.fetchall()
|
|
479
|
+
cursor.close()
|
|
480
|
+
if accounts is not None:
|
|
481
|
+
for account in accounts:
|
|
482
|
+
print("Username:", account[1], f'<{perms.get_perms(account[1])[0][0]}>', " Email:", account[2], " Created At:", account[5])
|
|
483
|
+
else:
|
|
484
|
+
print("No accounts found.")
|
|
485
|
+
|
|
486
|
+
@db_connection
|
|
487
|
+
def get_obfuscated_device_reservations_grouped_past_days(self, days: int) -> dict[str, dict[str, int]]:
|
|
488
|
+
try:
|
|
489
|
+
days = int(days)
|
|
490
|
+
except Exception:
|
|
491
|
+
return {}
|
|
492
|
+
|
|
493
|
+
if days < 0:
|
|
494
|
+
return {}
|
|
495
|
+
|
|
496
|
+
now = datetime.now()
|
|
497
|
+
cutoff = now - timedelta(days=days)
|
|
498
|
+
|
|
499
|
+
def _parse_logged_int(v) -> int:
|
|
500
|
+
if isinstance(v, int):
|
|
501
|
+
return v
|
|
502
|
+
if isinstance(v, float):
|
|
503
|
+
return int(v)
|
|
504
|
+
|
|
505
|
+
s = str(v).strip()
|
|
506
|
+
|
|
507
|
+
# Handles strings like:
|
|
508
|
+
# "int64_value: 2"
|
|
509
|
+
# "int64_value: 1772587800"
|
|
510
|
+
if ":" in s:
|
|
511
|
+
s = s.split(":", 1)[1].strip()
|
|
512
|
+
|
|
513
|
+
# remove quotes/newlines if present
|
|
514
|
+
s = s.strip().strip('"').strip("'")
|
|
515
|
+
return int(float(s))
|
|
516
|
+
|
|
517
|
+
# Take a consistent snapshot of the log file under the same lock used by writers.
|
|
518
|
+
try:
|
|
519
|
+
with log_lock:
|
|
520
|
+
activity_log = _load_logs()
|
|
521
|
+
except Exception:
|
|
522
|
+
activity_log = []
|
|
523
|
+
|
|
524
|
+
out: dict[str, dict[str, int]] = {}
|
|
525
|
+
|
|
526
|
+
for entry in activity_log:
|
|
527
|
+
try:
|
|
528
|
+
if not isinstance(entry, dict):
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
if str(entry.get("action", "")).strip() != "reserve_device":
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
details = entry.get("details", {})
|
|
535
|
+
if not isinstance(details, dict):
|
|
536
|
+
continue
|
|
537
|
+
|
|
538
|
+
args = details.get("args", {})
|
|
539
|
+
result = details.get("result", {})
|
|
540
|
+
|
|
541
|
+
if not isinstance(args, dict) or not isinstance(result, dict):
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
# Only count SUCCESSFUL reservations
|
|
545
|
+
if "Token" not in result:
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
raw_device_id = args.get("dd")
|
|
549
|
+
raw_start = args.get("st")
|
|
550
|
+
raw_end = args.get("et")
|
|
551
|
+
|
|
552
|
+
if raw_device_id is None or raw_start is None or raw_end is None:
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
device_id = _parse_logged_int(raw_device_id)
|
|
556
|
+
start_ts = _parse_logged_int(raw_start)
|
|
557
|
+
end_ts = _parse_logged_int(raw_end)
|
|
558
|
+
|
|
559
|
+
start_dt = datetime.fromtimestamp(start_ts)
|
|
560
|
+
end_dt = datetime.fromtimestamp(end_ts)
|
|
561
|
+
|
|
562
|
+
# Same overlap semantics as before
|
|
563
|
+
if end_dt < cutoff or start_dt > now:
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
dev_key = str(device_id)
|
|
567
|
+
day_key = start_dt.date().isoformat()
|
|
568
|
+
|
|
569
|
+
if dev_key not in out:
|
|
570
|
+
out[dev_key] = {}
|
|
571
|
+
|
|
572
|
+
out[dev_key][day_key] = out[dev_key].get(day_key, 0) + 1
|
|
573
|
+
|
|
574
|
+
except Exception:
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
return dict(sorted(out.items(), key=lambda kv: int(kv[0])))
|
|
578
|
+
|
|
579
|
+
@db_connection
|
|
580
|
+
def get_obfuscated_usage_summary(self) -> dict[str, int | float]:
|
|
581
|
+
|
|
582
|
+
int_re = re.compile(r'int64_value:\s*([0-9]+)')
|
|
583
|
+
str_re = re.compile(r'string_value:\s*"([^"]*)"')
|
|
584
|
+
|
|
585
|
+
def _parse_proto_value(v):
|
|
586
|
+
if isinstance(v, (int, float)):
|
|
587
|
+
return v
|
|
588
|
+
if not isinstance(v, str):
|
|
589
|
+
return v
|
|
590
|
+
|
|
591
|
+
m_int = int_re.search(v)
|
|
592
|
+
if m_int:
|
|
593
|
+
return int(m_int.group(1))
|
|
594
|
+
|
|
595
|
+
m_str = str_re.search(v)
|
|
596
|
+
if m_str:
|
|
597
|
+
return m_str.group(1)
|
|
598
|
+
|
|
599
|
+
return v
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
with log_lock:
|
|
603
|
+
activity_log = _load_logs()
|
|
604
|
+
except Exception:
|
|
605
|
+
activity_log = []
|
|
606
|
+
|
|
607
|
+
accounts: set[str] = set()
|
|
608
|
+
total_reservations = 0
|
|
609
|
+
total_reserved_seconds = 0
|
|
610
|
+
|
|
611
|
+
for entry in activity_log:
|
|
612
|
+
try:
|
|
613
|
+
if not isinstance(entry, dict):
|
|
614
|
+
continue
|
|
615
|
+
|
|
616
|
+
action = entry.get("action")
|
|
617
|
+
username = entry.get("username")
|
|
618
|
+
details = entry.get("details", {}) or {}
|
|
619
|
+
args = details.get("args", {}) or {}
|
|
620
|
+
result = details.get("result", {}) or {}
|
|
621
|
+
|
|
622
|
+
if not isinstance(args, dict) or not isinstance(result, dict):
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
# Count accounts from successful logins
|
|
626
|
+
if action == "login" and "UC" in result:
|
|
627
|
+
uc_name = _parse_proto_value(result.get("UC"))
|
|
628
|
+
if uc_name:
|
|
629
|
+
accounts.add(str(uc_name))
|
|
630
|
+
elif username:
|
|
631
|
+
accounts.add(str(username))
|
|
632
|
+
|
|
633
|
+
# Count reservations + reserved time
|
|
634
|
+
if action == "reserve_device":
|
|
635
|
+
total_reservations += 1
|
|
636
|
+
|
|
637
|
+
st = _parse_proto_value(args.get("st"))
|
|
638
|
+
et = _parse_proto_value(args.get("et"))
|
|
639
|
+
|
|
640
|
+
if isinstance(st, int) and isinstance(et, int) and et > st:
|
|
641
|
+
total_reserved_seconds += (et - st)
|
|
642
|
+
|
|
643
|
+
except Exception:
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
"account_count": len(accounts),
|
|
648
|
+
"total_reservations": int(total_reservations),
|
|
649
|
+
"total_reserved_hours": round(total_reserved_seconds / 3600.0, 1),
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
def get_reservation_count(self, username):
|
|
653
|
+
self.cursor.execute("SELECT COUNT(*) FROM Reservations WHERE username = ?", (username,))
|
|
654
|
+
count = self.cursor.fetchone()[0]
|
|
655
|
+
return count
|
|
656
|
+
|
|
657
|
+
# @db_connection
|
|
658
|
+
# def update_devices(self):
|
|
659
|
+
# devices = get_all_devices()
|
|
660
|
+
# for device_id in list(devices.keys()):
|
|
661
|
+
# self._clean_expired_reservations(device_id)
|
|
662
|
+
|
|
663
|
+
# self.cursor.execute('SELECT * FROM Reservations')
|
|
664
|
+
# reservations = self.cursor.fetchall()
|
|
665
|
+
|
|
666
|
+
# reservations_now = 0
|
|
667
|
+
|
|
668
|
+
# for reservation in reservations: # Update tokens for each device
|
|
669
|
+
# if datetime.strptime(reservation[3], '%Y-%m-%d %H:%M:%S') <= datetime.now() and datetime.strptime(reservation[4], '%Y-%m-%d %H:%M:%S') > datetime.now():
|
|
670
|
+
# del devices[reservation[2]]
|
|
671
|
+
# set_device(reservation[2], reservation[5], reservation[6])
|
|
672
|
+
|
|
673
|
+
# # if not get_transmitter_state(): # Turn transmitter on
|
|
674
|
+
# # start_transmitter()
|
|
675
|
+
|
|
676
|
+
# reservations_now += 1
|
|
677
|
+
|
|
678
|
+
# if reservations_now == 0: # Turn transmitter off if no resservations
|
|
679
|
+
# # terminate_transmitter()
|
|
680
|
+
# pass
|
|
681
|
+
|
|
682
|
+
# for device in devices:
|
|
683
|
+
# set_device(device, '', '')
|
|
684
|
+
|
|
685
|
+
from datetime import datetime
|
|
686
|
+
|
|
687
|
+
@db_connection
|
|
688
|
+
def update_devices(self):
|
|
689
|
+
devices = get_all_devices() # whatever your local inventory is
|
|
690
|
+
|
|
691
|
+
# Build a lookup: canonical string -> original key in `devices`
|
|
692
|
+
# (handles int keys, str keys, etc.)
|
|
693
|
+
key_by_str = {str(k).strip(): k for k in devices.keys()}
|
|
694
|
+
|
|
695
|
+
# Clean expired reservations for devices we actually know locally
|
|
696
|
+
for k in list(devices.keys()):
|
|
697
|
+
self._clean_expired_reservations(str(k).strip())
|
|
698
|
+
|
|
699
|
+
self.cursor.execute("SELECT * FROM Reservations")
|
|
700
|
+
reservations = self.cursor.fetchall()
|
|
701
|
+
|
|
702
|
+
now = datetime.now()
|
|
703
|
+
reservations_now = 0
|
|
704
|
+
|
|
705
|
+
for reservation in reservations:
|
|
706
|
+
start = datetime.strptime(reservation[3], "%Y-%m-%d %H:%M:%S")
|
|
707
|
+
end = datetime.strptime(reservation[4], "%Y-%m-%d %H:%M:%S")
|
|
708
|
+
|
|
709
|
+
if start <= now < end:
|
|
710
|
+
dev_id = str(reservation[2]).strip()
|
|
711
|
+
|
|
712
|
+
# If this reservation corresponds to a local device, remove it from the "clear later" list.
|
|
713
|
+
orig_key = key_by_str.pop(dev_id, None)
|
|
714
|
+
if orig_key is not None:
|
|
715
|
+
devices.pop(orig_key, None) # safe even if already removed
|
|
716
|
+
|
|
717
|
+
# Apply reservation token/state for this device_id (local or virtual)
|
|
718
|
+
set_device(dev_id, reservation[5] or "", reservation[6] or "")
|
|
719
|
+
|
|
720
|
+
reservations_now += 1
|
|
721
|
+
|
|
722
|
+
if reservations_now == 0:
|
|
723
|
+
# terminate_transmitter()
|
|
724
|
+
pass
|
|
725
|
+
|
|
726
|
+
# Clear tokens for remaining *local* devices that are not currently reserved
|
|
727
|
+
for k in devices.keys():
|
|
728
|
+
set_device(str(k).strip(), "", "")
|
|
729
|
+
|
|
730
|
+
start_transmitter()
|
|
731
|
+
reservation_handler = ReservationHandler()
|
|
732
|
+
|
|
733
|
+
# region Logging purposes
|
|
734
|
+
|
|
735
|
+
import numpy as np
|
|
736
|
+
import threading
|
|
737
|
+
from datetime import datetime
|
|
738
|
+
from pathlib import Path
|
|
739
|
+
|
|
740
|
+
# Path to your activity logs file
|
|
741
|
+
LOG_PATH = get_db_dir() / "activity_logs.npy"
|
|
742
|
+
log_lock = threading.Lock()
|
|
743
|
+
|
|
744
|
+
def _load_logs():
|
|
745
|
+
if LOG_PATH.exists():
|
|
746
|
+
return np.load(LOG_PATH, allow_pickle=True).tolist()
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
def _save_logs(logs):
|
|
750
|
+
np.save(LOG_PATH, logs, allow_pickle=True)
|
|
751
|
+
|
|
752
|
+
def _append_log(entry: dict):
|
|
753
|
+
"""Append a single log entry. Holds log_lock only during file I/O, not during the RPC call."""
|
|
754
|
+
with log_lock:
|
|
755
|
+
logs = _load_logs()
|
|
756
|
+
logs.append(entry)
|
|
757
|
+
_save_logs(logs)
|
|
758
|
+
|
|
759
|
+
# Store a reference to the original handle_call so we can wrap it
|
|
760
|
+
ReservationHandler._original_handle_call = ReservationHandler.handle_call
|
|
761
|
+
|
|
762
|
+
def sanitize_for_logging(obj):
|
|
763
|
+
"""
|
|
764
|
+
Recursively converts non-primitive objects into a string
|
|
765
|
+
representation, so that the object becomes pickleable.
|
|
766
|
+
"""
|
|
767
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
768
|
+
return obj
|
|
769
|
+
elif isinstance(obj, dict):
|
|
770
|
+
return {sanitize_for_logging(key): sanitize_for_logging(value) for key, value in obj.items()}
|
|
771
|
+
elif isinstance(obj, list):
|
|
772
|
+
return [sanitize_for_logging(item) for item in obj]
|
|
773
|
+
elif isinstance(obj, tuple):
|
|
774
|
+
return tuple(sanitize_for_logging(item) for item in obj)
|
|
775
|
+
else:
|
|
776
|
+
return repr(obj)
|
|
777
|
+
|
|
778
|
+
def handle_call_wrapper(self, *, function_name, args):
|
|
779
|
+
"""
|
|
780
|
+
Wrapper around the original handle_call.
|
|
781
|
+
The RPC call runs WITHOUT holding log_lock so a slow/stalled disk write
|
|
782
|
+
can never block the server. Logging happens after the call returns.
|
|
783
|
+
"""
|
|
784
|
+
# Make a sanitized copy of args before the call (no lock needed here)
|
|
785
|
+
sanitized_args = sanitize_for_logging(dict(args))
|
|
786
|
+
if 'pw' in sanitized_args:
|
|
787
|
+
sanitized_args['pw'] = '*'
|
|
788
|
+
|
|
789
|
+
# Run the actual RPC — NOT inside log_lock
|
|
790
|
+
result = self._original_handle_call(function_name=function_name, args=args)
|
|
791
|
+
|
|
792
|
+
# Log asynchronously after the call completes
|
|
793
|
+
sanitized_result = sanitize_for_logging(result)
|
|
794
|
+
username = sanitize_for_logging(unmap_arg(args['un'])) if 'un' in args else 'N/A'
|
|
795
|
+
entry = {
|
|
796
|
+
"timestamp": datetime.now().isoformat(),
|
|
797
|
+
"action": function_name,
|
|
798
|
+
"username": username,
|
|
799
|
+
"details": {
|
|
800
|
+
"args": sanitized_args,
|
|
801
|
+
"result": sanitized_result
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
threading.Thread(target=_append_log, args=(entry,), daemon=True).start()
|
|
805
|
+
|
|
806
|
+
return result
|
|
807
|
+
|
|
808
|
+
# Replace the original handle_call with our wrapper
|
|
809
|
+
ReservationHandler.handle_call = handle_call_wrapper
|
|
810
|
+
|
|
811
|
+
# endregion
|