remoterf 0.0.7.41__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.

Potentially problematic release.


This version of remoterf might be problematic. Click here for more details.

remoteRF/core/app.py ADDED
@@ -0,0 +1,508 @@
1
+ from . import *
2
+ from ..common.utils import *
3
+
4
+ import getpass
5
+ import os
6
+ import datetime
7
+ import time
8
+ import ast
9
+
10
+ from prompt_toolkit import PromptSession
11
+
12
+ account = RemoteRFAccount()
13
+ session = PromptSession()
14
+
15
+ def welcome():
16
+ printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), f"\nCurrent version: {print_my_version()} \nAll times are in Pacific Time (Los Angeles)", (Sty.GRAY))
17
+ try:
18
+ inpu = session.prompt(stylize("Please ", Sty.DEFAULT, "login", Sty.GREEN, " or ", Sty.DEFAULT, "register", Sty.RED, " to continue. (", Sty.DEFAULT, 'l', Sty.GREEN, "/", Sty.DEFAULT, 'r', Sty.RED, "): ", Sty.DEFAULT))
19
+ if inpu == 'r':
20
+ print("Registering new account ...")
21
+ account.username = input("Username: ")
22
+ double_check = True
23
+ while double_check:
24
+ password = getpass.getpass("Password (Hidden): ")
25
+ password2 = getpass.getpass("Confirm Password: ")
26
+ if password == password2:
27
+ double_check = False
28
+ else:
29
+ print("Passwords do not match. Try again.")
30
+
31
+ account.password = password
32
+ account.email = input("Email: ") # TODO: Email verification.
33
+ # check if login was valid
34
+ os.system('cls' if os.name == 'nt' else 'clear')
35
+
36
+ if not account.create_user():
37
+ welcome()
38
+ else:
39
+ account.username = input("Username: ")
40
+ account.password = getpass.getpass("Password (Hidden): ")
41
+ # check if login was valid
42
+ if not account.login_user():
43
+ os.system('cls' if os.name == 'nt' else 'clear')
44
+ print("Invalid login. Try again. Contact admin(s) if you forgot your password.")
45
+ welcome()
46
+ except KeyboardInterrupt:
47
+ exit()
48
+ except EOFError:
49
+ exit()
50
+
51
+ def title():
52
+ printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), f"\nCurrent version: {print_my_version()} \nAll times are in Pacific Time (Los Angeles)", (Sty.GRAY))
53
+ # printf(f"Logged in as: ", Sty.DEFAULT, f'{account.username}', Sty.MAGENTA)
54
+ printf(f"Input ", Sty.DEFAULT, "'help' ", Sty.BRIGHT_GREEN, "for a list of avaliable commands.", Sty.DEFAULT)
55
+
56
+ def commands():
57
+ printf("Commands:", Sty.BOLD)
58
+ printf("'clear' ", Sty.MAGENTA, " : Clear terminal", Sty.DEFAULT)
59
+ printf("'getdev' ", Sty.MAGENTA, " : View devices", Sty.DEFAULT)
60
+ printf("'help' or 'h' ", Sty.MAGENTA, " : Show this help message", Sty.DEFAULT)
61
+ printf("'perms' ", Sty.MAGENTA, " : View permissions", Sty.DEFAULT)
62
+ printf("'exit' or 'quit' ", Sty.MAGENTA, ": Exit", Sty.DEFAULT)
63
+ printf("'getres' ", Sty.MAGENTA, " : View all reservations", Sty.DEFAULT)
64
+ printf("'myres' ", Sty.MAGENTA, " : View my reservations", Sty.DEFAULT)
65
+ printf("'cancelres' ", Sty.MAGENTA, " : Cancel a reservation", Sty.DEFAULT)
66
+ printf("'resdev' ", Sty.MAGENTA, " : Reserve a device", Sty.DEFAULT)
67
+ # printf("'resdev -n' ", Sty.MAGENTA, "- naive reserve device", Sty.DEFAULT)
68
+ # printf("'resdev s' ", Sty.MAGENTA, "- Reserve a Device (by single date)", Sty.DEFAULT)
69
+ # check if user is admin
70
+ # if account.get_perms().results['UC'] == 'Admin':
71
+
72
+ def clear():
73
+ os.system('cls' if os.name == 'nt' else 'clear')
74
+ title()
75
+
76
+ def print_my_version():
77
+ import sys
78
+ latest = newest_version_pip("remoterf")
79
+ try:
80
+ import importlib.metadata as md # Py3.8+
81
+ top = __name__.split('.')[0]
82
+ # Try mapping package → distribution (Py3.10+); fall back to same name.
83
+ for dist in getattr(md, "packages_distributions", lambda: {})().get(top, []):
84
+ if (latest == md.version(dist)):
85
+ return f"{md.version(dist)} (LATEST)"
86
+ else:
87
+ return f"{md.version(dist)} (OUTDATED)"
88
+ return md.version(top)
89
+ except Exception:
90
+ # Last resort: __version__ attribute if you define it.
91
+ return getattr(sys.modules.get(__name__.split('.')[0]), "__version__", "unknown")
92
+
93
+ def newest_version_pip(project="remoterf"):
94
+ import sys, subprocess, re
95
+ out = subprocess.check_output(
96
+ [sys.executable, "-m", "pip", "index", "versions", project],
97
+ text=True, stderr=subprocess.STDOUT
98
+ )
99
+ m = re.search(r"(?i)\blatest\s*:\s*([^\s,]+)", out)
100
+ return m.group(1) if m else None
101
+
102
+
103
+ def reservations():
104
+ data = account.get_reservations()
105
+ if 'ace' in data.results:
106
+ print(f"Error: {unmap_arg(data.results['ace'])}")
107
+ return
108
+ entries = []
109
+
110
+ for key, value in data.results.items():
111
+ parts = unmap_arg(value).split(',')
112
+ # Create a dictionary for each entry with named fields
113
+ entry = {
114
+ 'username': parts[0],
115
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
116
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
117
+ 'end_time': parts[3]
118
+ }
119
+ entries.append(entry)
120
+
121
+ if (entries == []):
122
+ printf("No reservations found.", Sty.BOLD)
123
+ return
124
+
125
+ printf("Reservations:", Sty.BOLD)
126
+
127
+ # Sort the entries by device_id and then by start_time
128
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time']))
129
+
130
+ # Format the sorted entries into strings
131
+ for entry in sorted_entries:
132
+ printf(f'Device ID: ', Sty.RED, f'{entry["device_id"]}', Sty.MAGENTA, f', Start Time: ', Sty.RED, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f', End Time: ', Sty.RED, f'{entry["end_time"]}', Sty.BLUE)
133
+
134
+ def my_reservations():
135
+ data = account.get_reservations()
136
+ if 'ace' in data.results:
137
+ print(f"Error: {unmap_arg(data.results['ace'])}")
138
+ return
139
+ entries = []
140
+
141
+ for key, value in data.results.items():
142
+ parts = unmap_arg(value).split(',')
143
+ # Create a dictionary for each entry with named fields
144
+ entry = {
145
+ 'username': parts[0],
146
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
147
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
148
+ 'end_time': parts[3]
149
+ }
150
+ entries.append(entry)
151
+
152
+ if (entries == []):
153
+ printf("No reservations found.", Sty.BOLD)
154
+ return
155
+
156
+ printf("Reservations under: ", Sty.BOLD, f'{account.username}', Sty.MAGENTA)
157
+
158
+ # Sort the entries by device_id and then by start_time
159
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time']))
160
+
161
+ for entry in sorted_entries:
162
+ if account.username == entry['username']:
163
+ printf(f'Device ID: ', Sty.RED, f'{entry["device_id"]}', Sty.MAGENTA, f', Start Time: ', Sty.RED, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f', End Time: ', Sty.RED, f'{entry["end_time"]}', Sty.BLUE)
164
+
165
+ def cancel_my_reservation():
166
+ ## print all of ur reservations and their ids
167
+ ## ask for id to cancel
168
+ ## remove said reservation
169
+ data = account.get_reservations()
170
+ if 'ace' in data.results:
171
+ print(f"Error: {unmap_arg(data.results['ace'])}")
172
+ return
173
+
174
+ entries:list = []
175
+
176
+ for key, value in data.results.items():
177
+ parts = unmap_arg(value).split(',')
178
+ # Create a dictionary for each entry with named fields
179
+ entry = {
180
+ 'id': -1,
181
+ 'internal_id': key,
182
+ 'username': parts[0],
183
+ 'device_id': int(parts[1]), # Convert device_id to integer for proper numerical sorting
184
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'), # Convert start_time to datetime
185
+ 'end_time': parts[3]
186
+ }
187
+ if account.username == entry['username']:
188
+ entries.append(entry)
189
+
190
+ printf("Current Reservation(s) under ", Sty.BOLD, f'{account.username}:', Sty.MAGENTA)
191
+
192
+ sorted_entries = sorted(entries, key=lambda x: (x['device_id'], x['start_time'])) # sort by device_id and start_time
193
+ for i, entry in enumerate(sorted_entries): # label all reservations with unique id
194
+ entry['id'] = i
195
+ printf(f'Reservation ID: ', Sty.GRAY, f'{i}', Sty.MAGENTA, f' Device ID: ', Sty.GRAY, f'{entry["device_id"]}', Sty.BRIGHT_GREEN, f' Start Time: ', Sty.GRAY, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f' End Time: ', Sty.GRAY, f'{entry["end_time"]}', Sty.BLUE)
196
+ # print(f"Reservation ID {i}, Device ID: {entry['device_id']}, Start Time: {entry['start_time'].strftime('%Y-%m-%d %H:%M:%S')}, End Time: {entry['end_time']}")
197
+
198
+ if sorted_entries == []:
199
+ printf("No reservations found.", Sty.BOLD)
200
+ return
201
+
202
+ inpu = session.prompt(stylize("Enter the ID of the reservation you would like to cancel ", Sty.BOLD, '(abort with any non number key input)', Sty.RED, ': ', Sty.BOLD))
203
+
204
+ if inpu.isdigit():
205
+ id = int(inpu)
206
+ if id >= len(sorted_entries):
207
+ print("Invalid ID.")
208
+ return
209
+
210
+ # grab the reservation
211
+ for entry in sorted_entries:
212
+ if entry['id'] == id:
213
+ db_id = entry['internal_id']
214
+ if session.prompt(stylize(f'Cancel reservation ID ', Sty.DEFAULT, f'{id}', Sty.MAGENTA, f' Device ID: ', Sty.DEFAULT, f'{entry["device_id"]}', Sty.BRIGHT_GREEN, f' Start Time: ', Sty.GRAY, f'{entry["start_time"].strftime("%Y-%m-%d %H:%M:%S")}', Sty.BLUE, f' End Time: ', Sty.DEFAULT, f'{entry["end_time"]}', Sty.BLUE, f' ? (y/n):', Sty.DEFAULT)) == 'y':
215
+ response = account.cancel_reservation(db_id)
216
+ if 'ace' in response.results:
217
+ print(f"Error: {unmap_arg(response.results['ace'])}")
218
+ elif 'UC' in response.results:
219
+ printf(f"Reservation ID ", Sty.DEFAULT, f'{id}', Sty.BRIGHT_BLUE, ' successfully canceled.', Sty.DEFAULT)
220
+ else:
221
+ print("Aborting. User canceled action.")
222
+ return
223
+
224
+ print(f"Error: No reservation found with ID {id}.")
225
+ else:
226
+ print("Aborting. A non integer key was given.")
227
+
228
+ def devices():
229
+ data = account.get_devices()
230
+ if 'ace' in data.results:
231
+ print(f"Error: {unmap_arg(data.results['ace'])}")
232
+ return
233
+ printf("Devices:", Sty.BOLD)
234
+
235
+ for key in sorted(data.results, key=int):
236
+ printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{unmap_arg(data.results[key])}", Sty.GRAY)
237
+
238
+ def get_datetime(question:str):
239
+ timestamp = session.prompt(stylize(f'{question}', Sty.DEFAULT, ' (YYYY-MM-DD HH:MM): ', Sty.GRAY))
240
+ return datetime.datetime.strptime(timestamp + ':00', '%Y-%m-%d %H:%M:%S')
241
+
242
+ def reserve():
243
+ try:
244
+ id = session.prompt(stylize("Enter the device ID you would like to reserve: ", Sty.DEFAULT))
245
+ token = account.reserve_device(int(id), get_datetime("Reserve Start Time"), get_datetime("Reserve End Time"))
246
+ if token != '':
247
+ printf(f"Reservation successful. Thy Token -> ", Sty.BOLD, f"{token}", Sty.BG_GREEN)
248
+ printf(f"Please keep this token safe, as it is not saved on server side, and cannot be regenerated/reretrieved. ", Sty.DEFAULT)
249
+ except Exception as e:
250
+ printf(f"Error: {e}", Sty.BRIGHT_RED)
251
+
252
+ def perms():
253
+ data = account.get_perms()
254
+ if 'ace' in data.results:
255
+ print(f"Error: {unmap_arg(data.results['ace'])}")
256
+ return
257
+
258
+ results = ast.literal_eval(unmap_arg(data.results['UC']))[0]
259
+ printf(f'Permission Level: ', Sty.BOLD, f'{results[0]}', Sty.BLUE)
260
+ if results[0] == 'Normal User':
261
+ print(unmap_arg(data.results['details']))
262
+ elif results[0] == 'Power User':
263
+ printf(f'Max Reservations: ', Sty.DEFAULT, f'{results[3]}', Sty.MAGENTA)
264
+ printf(f'Max Reservation Duration (min): ', Sty.DEFAULT, f'{int(results[4]/60)}', Sty.MAGENTA)
265
+ printf(f'Device IDs allowed Access to: ', Sty.DEFAULT, f'{results[5]}', Sty.MAGENTA)
266
+ elif results[0] == 'Admin':
267
+ printf(f'No restrictions on reservation count or duration.', Sty.DEFAULT)
268
+ else:
269
+ printf(f"Error: Unknown permission level {results[0]}", Sty.BRIGHT_RED)
270
+
271
+ # New block scheduling
272
+
273
+ def fetch_all_reservations():
274
+ data = account.get_reservations()
275
+ if 'ace' in data.results:
276
+ print(f"Error: {unmap_arg(data.results['ace'])}")
277
+ return []
278
+ entries = []
279
+
280
+ for key, value in data.results.items():
281
+ parts = unmap_arg(value).split(',')
282
+ # Convert both start and end times to datetime objects.
283
+ entry = {
284
+ 'username': parts[0],
285
+ 'device_id': int(parts[1]), # Stored as an int
286
+ 'start_time': datetime.datetime.strptime(parts[2], '%Y-%m-%d %H:%M:%S'),
287
+ 'end_time': datetime.datetime.strptime(parts[3], '%Y-%m-%d %H:%M:%S')
288
+ }
289
+ entries.append(entry)
290
+ return entries
291
+
292
+ def fetch_reservations_for_range(start_day: datetime.date, end_day: datetime.date):
293
+ """
294
+ Fetch all reservations (via fetch_all_reservations) and filter those whose start_time date falls between start_day and end_day (inclusive).
295
+ Returns a dictionary keyed by (device_id, day) (device_id as string, day as datetime.date) with a list of reservation tuples.
296
+ """
297
+ all_res = fetch_all_reservations() # This calls the network only once.
298
+ res_dict = {}
299
+ for res in all_res:
300
+ res_day = res['start_time'].date()
301
+ if start_day <= res_day <= end_day:
302
+ key = (str(res['device_id']), res_day)
303
+ res_dict.setdefault(key, []).append((res['start_time'], res['end_time']))
304
+ return res_dict
305
+
306
+ def is_slot_conflicting(slot: tuple, reservations: list):
307
+ """Return True if the slot overlaps with any reservation in the provided list."""
308
+ slot_start, slot_end = slot
309
+ for res_start, res_end in reservations:
310
+ if slot_start < res_end and slot_end > res_start:
311
+ return True
312
+ return False
313
+
314
+ def interactive_reserve_next_days(block_minutes=60):
315
+ """
316
+ Interactive function that:
317
+ 1) Displays a menu of all devices (0-based indexing).
318
+ 2) Prompts the user for which device they want.
319
+ 3) Prompts how many days (starting today) to check for available reservations.
320
+ 4) Prompts for an optional block duration in minutes (default = 60).
321
+ 5) Displays the free time slots (in 'block_minutes' increments) for that device,
322
+ over the indicated number of days, also 0-based indexed.
323
+ 6) Reserves the chosen slot on that device, after confirmation.
324
+ """
325
+ try:
326
+ # --- 1) Fetch and display all devices ---
327
+ data = account.get_devices()
328
+ if 'ace' in data.results:
329
+ print(f"Error: {unmap_arg(data.results['ace'])}")
330
+ return
331
+
332
+ print("Devices:")
333
+ # Sort devices by integer key
334
+ sorted_device_ids = sorted(data.results.keys(), key=int)
335
+
336
+ for idx, dev_id in enumerate(sorted_device_ids):
337
+ dev_name = unmap_arg(data.results[dev_id])
338
+ print(f"{idx}. Device ID: {dev_id} Name: {dev_name}")
339
+
340
+ # --- 2) Prompt user to pick a device by 0-based index ---
341
+ device_selection = input("Which device do you want? (enter the 0-based index): ")
342
+ try:
343
+ device_selection = int(device_selection)
344
+ if device_selection < 0 or device_selection >= len(sorted_device_ids):
345
+ print("Invalid selection.")
346
+ return
347
+ except ValueError:
348
+ print("Invalid input. Please enter a number.")
349
+ return
350
+
351
+ chosen_device_id = sorted_device_ids[device_selection]
352
+
353
+ # --- 3) Prompt user for the number of days ---
354
+ num_days = int(input("Enter the number of days to check for available reservations (starting today): "))
355
+
356
+ # --- 4) Optionally override block_minutes ---
357
+ # user_block_input = input(f"Enter block duration in minutes (e.g. 15, 30, 60, 120). Press Enter to default ({block_minutes}): ").strip()
358
+ # if user_block_input:
359
+ # try:
360
+ # block_minutes = int(user_block_input)
361
+ # except ValueError:
362
+ # print("Invalid block duration. Using default of 60 minutes.")
363
+ # block_minutes = 60
364
+
365
+ # --- 5) Find all free time slots for the chosen device over the next `num_days` days ---
366
+
367
+ today = datetime.date.today()
368
+ end_day = today + datetime.timedelta(days=num_days - 1)
369
+
370
+ # We only need one call to fetch reservations for the range:
371
+ reservations_range = fetch_reservations_for_range(today, end_day)
372
+
373
+ # We'll keep a list of (day, (slot_start, slot_end)) for which the device is free
374
+ available_slots = []
375
+
376
+ now = datetime.datetime.now()
377
+
378
+ # Helper to build time slots of length 'block_minutes' starting at 00:00 up to 24:00
379
+ def build_time_slots(date: datetime.date, block_size: int):
380
+ slots = []
381
+ start_of_day = datetime.datetime.combine(date, datetime.time(0, 0))
382
+ minutes_in_day = 24 * 60 # 1440
383
+ current_offset = 0
384
+ while current_offset < minutes_in_day:
385
+ slot_start = start_of_day + datetime.timedelta(minutes=current_offset)
386
+ slot_end = slot_start + datetime.timedelta(minutes=block_size)
387
+ # Stop if slot_end bleeds into the next calendar day
388
+ if slot_end.date() != date and slot_end.time() != datetime.time.min:
389
+ break
390
+ slots.append((slot_start, slot_end))
391
+ current_offset += block_size
392
+ return slots
393
+
394
+ # Build free slots for each day in [today, end_day]
395
+ for i in range(num_days):
396
+ day = today + datetime.timedelta(days=i)
397
+ all_slots = build_time_slots(day, block_minutes)
398
+
399
+ # The reservations for the chosen device on this day:
400
+ key = (str(chosen_device_id), day)
401
+ day_reservations = reservations_range.get(key, [])
402
+
403
+ for slot in all_slots:
404
+ slot_start, slot_end = slot
405
+ # Skip if it's for "today" and the slot ends in the past
406
+ if day == today and slot_end <= now:
407
+ continue
408
+
409
+ # Check for conflict
410
+ if not is_slot_conflicting(slot, day_reservations):
411
+ available_slots.append((day, slot))
412
+
413
+ if not available_slots:
414
+ print(f"No available time slots for device {chosen_device_id} in the next {num_days} days.")
415
+ return
416
+
417
+ # Sort by day, then by slot start time
418
+ available_slots.sort(key=lambda x: (x[0], x[1][0]))
419
+
420
+ # --- Display the available slots, using 0-based index ---
421
+ print(f"\nAvailable time slots for device {chosen_device_id} over the next {num_days} days:")
422
+ last_day = None
423
+ for idx, (day, slot) in enumerate(available_slots):
424
+ slot_start_str = slot[0].strftime('%I:%M %p')
425
+ slot_end_str = slot[1].strftime('%I:%M %p')
426
+ if day != last_day:
427
+ # Print a header for the day
428
+ day_header = f"{day.strftime('%Y-%m-%d')} ({day.strftime('%a')}) {day.strftime('%b')}. {day.day}"
429
+ print("\n" + day_header)
430
+ last_day = day
431
+
432
+ print(f" {idx}. {slot_start_str} - {slot_end_str}")
433
+
434
+ # Prompt user to pick a slot by 0-based index
435
+ selection = input("Select a slot by index: ")
436
+ try:
437
+ selection = int(selection)
438
+ if selection < 0 or selection >= len(available_slots):
439
+ print("Invalid selection.")
440
+ return
441
+ except ValueError:
442
+ print("Invalid input. Please enter a number.")
443
+ return
444
+
445
+ chosen_day, chosen_slot = available_slots[selection]
446
+ slot_start_str = chosen_slot[0].strftime('%I:%M %p')
447
+ slot_end_str = chosen_slot[1].strftime('%I:%M %p')
448
+
449
+ confirmation = input(
450
+ f"You have selected a reservation on {chosen_day.strftime('%Y-%m-%d')} "
451
+ f"from {slot_start_str} to {slot_end_str} on device {chosen_device_id}. "
452
+ f"Confirm reservation? (y/n): "
453
+ ).strip().lower()
454
+
455
+ if confirmation != 'y':
456
+ print("Reservation cancelled.")
457
+ return
458
+
459
+ # print(f"device_id : {chosen_device_id}, start_time : {chosen_slot[0]}, end_time : {chosen_slot[1]}")
460
+
461
+ # --- 6) Reserve the chosen slot on the chosen device ---
462
+ token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
463
+ if token:
464
+ print(f"Reservation successful on device {chosen_device_id} for "
465
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.")
466
+ print(f"Thy Token -> {token}")
467
+ print("Please keep this token safe, as it is not saved on server side "
468
+ "and cannot be retrieved again.")
469
+
470
+ except Exception as e:
471
+ print(f"Error: {e}")
472
+
473
+ welcome()
474
+ clear()
475
+
476
+ while True:
477
+ try:
478
+ inpu = session.prompt(stylize(f'{account.username}@remote_rf: ', Sty.BLUE))
479
+ if inpu == "clear":
480
+ clear()
481
+ elif inpu == "getdev":
482
+ devices()
483
+ elif inpu == "help" or inpu == "h":
484
+ commands()
485
+ elif inpu == "perms":
486
+ perms()
487
+ elif inpu == "quit" or inpu == "exit":
488
+ break
489
+ elif inpu == "getres":
490
+ reservations()
491
+ elif inpu == "myres":
492
+ my_reservations()
493
+ # elif inpu == "resdev s":
494
+ # interactive_reserve_all()
495
+ elif inpu == "resdev":
496
+ interactive_reserve_next_days(block_minutes=30)
497
+ elif inpu == 'cancelres':
498
+ cancel_my_reservation()
499
+ elif inpu == 'resdev -n':
500
+ # check if user is admin
501
+ # if account.get_perms().results['UC'] == 'Admin':
502
+ reserve()
503
+ else:
504
+ print(f"Unknown command: {inpu}")
505
+ except KeyboardInterrupt:
506
+ break
507
+ except EOFError:
508
+ break
@@ -0,0 +1,140 @@
1
+ # cert_fetcher.py
2
+ """
3
+ RemoteRF Client-Side Cert Fetcher (bootstrap)
4
+
5
+ Purpose:
6
+ - Fetch the public CA certificate (ca.crt) from a server's bootstrap endpoint
7
+ (your cert_provider) and save it locally for later use in:
8
+ grpc.ssl_channel_credentials(root_certificates=ca_bytes)
9
+
10
+ Security note (TOFU):
11
+ - This fetch is typically done over plaintext HTTP (or raw TCP). The CA cert is public,
12
+ but authenticity is not guaranteed on first fetch. If you want TOFU done "right",
13
+ you should display the SHA256 fingerprint and require user confirmation elsewhere.
14
+ This module focuses only on: fetch + save + return success.
15
+
16
+ Behavior:
17
+ - Attempts HTTP GET first: http://{host}:{port}/ca.crt
18
+ - If HTTP fails, falls back to raw TCP (reads until EOF).
19
+ - Saves to a per-profile file under ~/.config/remoterf/certs/ by default.
20
+
21
+ Exported API:
22
+ - fetch_and_save_ca_cert(host, port, *, out_path=None, profile=None, timeout_sec=3.0) -> bool
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import os
29
+ import socket
30
+ import urllib.request
31
+ from pathlib import Path
32
+ from typing import Optional
33
+
34
+
35
+ CERT_FILENAME_DEFAULT = "ca.crt"
36
+
37
+
38
+ def _default_config_dir() -> Path:
39
+ # Cross-platform-ish default; adjust if you already have a standard in your project.
40
+ # Linux: ~/.config/remoterf
41
+ # macOS: also acceptable; if you want Apple-standard, use ~/Library/Application Support/remoterf
42
+ base = Path(os.path.expanduser("~")) / ".config" / "remoterf"
43
+ return base
44
+
45
+
46
+ def _ensure_parent_dir(p: Path) -> None:
47
+ p.parent.mkdir(parents=True, exist_ok=True)
48
+
49
+
50
+ def _looks_like_pem_cert(data: bytes) -> bool:
51
+ return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
52
+
53
+
54
+ def sha256_fingerprint_pem(pem_bytes: bytes) -> str:
55
+ h = hashlib.sha256(pem_bytes).hexdigest()
56
+ return ":".join(h[i:i+2] for i in range(0, len(h), 2))
57
+
58
+
59
+ def _fetch_http(host: str, port: int, timeout_sec: float) -> bytes:
60
+ url = f"http://{host}:{port}/ca.crt"
61
+ req = urllib.request.Request(url, method="GET")
62
+ with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
63
+ data = resp.read()
64
+ return data
65
+
66
+
67
+ def _fetch_raw_tcp(host: str, port: int, timeout_sec: float) -> bytes:
68
+ chunks: list[bytes] = []
69
+ with socket.create_connection((host, port), timeout=timeout_sec) as s:
70
+ s.settimeout(timeout_sec)
71
+ while True:
72
+ try:
73
+ b = s.recv(4096)
74
+ except socket.timeout:
75
+ break
76
+ if not b:
77
+ break
78
+ chunks.append(b)
79
+ return b"".join(chunks)
80
+
81
+
82
+ def fetch_and_save_ca_cert(
83
+ host: str,
84
+ port: int,
85
+ *,
86
+ out_path: Optional[str | Path] = None,
87
+ profile: Optional[str] = None,
88
+ timeout_sec: float = 3.0,
89
+ overwrite: bool = True,
90
+ ) -> bool:
91
+ """
92
+ Fetch CA cert from the server bootstrap endpoint and save it to disk.
93
+
94
+ Args:
95
+ host: server host/ip running cert_provider
96
+ port: cert_provider port (NOT the TLS gRPC port)
97
+ out_path: explicit output path (overrides profile/default location)
98
+ profile: if provided, saves as ~/.config/remoterf/certs/<profile>.crt
99
+ timeout_sec: network timeout
100
+ overwrite: whether to overwrite existing file
101
+
102
+ Returns:
103
+ True on success, False on any failure.
104
+ """
105
+ try:
106
+ if not isinstance(port, int):
107
+ port = int(port)
108
+
109
+ # Determine destination path
110
+ if out_path is not None:
111
+ dest = Path(out_path).expanduser().resolve()
112
+ else:
113
+ cfg = _default_config_dir()
114
+ certs_dir = cfg / "certs"
115
+ name = f"{profile}.crt" if profile else CERT_FILENAME_DEFAULT
116
+ dest = certs_dir / name
117
+
118
+ _ensure_parent_dir(dest)
119
+
120
+ if dest.exists() and not overwrite:
121
+ return True # already present, treat as success
122
+
123
+ # Fetch (HTTP first, then raw TCP fallback)
124
+ data = b""
125
+ try:
126
+ data = _fetch_http(host, port, timeout_sec)
127
+ except Exception:
128
+ data = _fetch_raw_tcp(host, port, timeout_sec)
129
+
130
+ if not data or not _looks_like_pem_cert(data):
131
+ return False
132
+
133
+ # Save
134
+ dest.write_bytes(data)
135
+
136
+ # Optional: you may want to return/print fingerprint, but requested API is bool.
137
+ return True
138
+
139
+ except Exception:
140
+ return False
File without changes
@@ -0,0 +1,32 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFmzCCA4OgAwIBAgIURcP62Zj3L/2bVIIdFu+UcAMklBQwDQYJKoZIhvcNAQEL
3
+ BQAwXTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
4
+ MRUwEwYDVQQKDAxDb21wYW55IE5hbWUxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv
5
+ bTAeFw0yNDA2MjcyMDQ3MTNaFw0yNTA2MjcyMDQ3MTNaMF0xCzAJBgNVBAYTAlVT
6
+ MQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMQ29tcGFu
7
+ eSBOYW1lMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEB
8
+ AQUAA4ICDwAwggIKAoICAQCc92USDLn60Ju8+/fIMMsgIRD71cfuO2R0nD0+Tfe7
9
+ QOvnGpY7xSmtFbzjXadqjjqM8JsADyOJzudYf+KhfbfMcX6WYj1Flx3XVrjws5Nf
10
+ XPSYYT/uo+vBKJl7rFCYdrGFPVTI8oBz9EsdURriXOGn+rhUP4vIcx/0SKYQvWgI
11
+ oJ8huQ1A7bgrfl2qhyjYa+4AhMAF+YtWSI4fsSR3hwb2wR2Xb8zenVvaTAoc0GvY
12
+ TgxQ5Xue2UD/qR/wezGDQozPIsLXAYiK7fIvHxq1rzhAOOIGEtyp2uMAoq7NtYfu
13
+ 5ZgRcoD/O6pAPKZKdpC6toGu3kqj2W7bAWAeZzMOCa3DxYCa+r+dW96fhGhA5HLc
14
+ 4rwZDVa3hK1d8n0NmkkX2LUdFT5KXuB6InyeXKK/ZKNEAnxSn3cdaqKZWr9Bxbsz
15
+ G9osCNOzN8IQGILTGdQlGirraCQM+P0SvVFj8C22Mo5tuBG1eal1vFvO9LMwl2GP
16
+ SnZ4QB8BXpwAVEpOWe72lLRE99+5GhZvWQAi4RQ+0Mn2xwyJY8gkrK5i5m8kHKzs
17
+ naIfpRrtZ9oo821WTisVYQq5U7QUbPaiE4xEOMEDTiOSatvxQMqu6OGDWLxlqcgv
18
+ Fh317R+H6dhn+hLTGe6wnCozPTHqZoqv664gTs7KFHUgI/gU/dBc3oI/VEyrmuTI
19
+ GwIDAQABo1MwUTAdBgNVHQ4EFgQU/iskMPrT7yToKDJudHOUHRhnRG0wHwYDVR0j
20
+ BBgwFoAU/iskMPrT7yToKDJudHOUHRhnRG0wDwYDVR0TAQH/BAUwAwEB/zANBgkq
21
+ hkiG9w0BAQsFAAOCAgEAJpU4vErO68CduIzqr925gmKzUeSOqoXCLJGvlbUl3HWa
22
+ ieba6oL33UjB/6s5BUHjqTtUiYa1hHoPsI6S9HC7cCaykdWzrtPOrXpF/G4clT8H
23
+ 3xkk7lQNwYH0+OP3ITbrY0OiqsTBjVY0ltKOOqzrzRMHcAw6Jc1Zp/7tYv9ZzqYx
24
+ NiwDYlGO8rhyGa2k1zNazJbh7YT0hBKPYkhqF/jheMFBwT3cpw6rJomP3rcSUaK2
25
+ 0DP+v4taW7xxfxf8vI2Xdhy9XLdqyVNq0btFxy2cAOj19zSjnHvPlwXSjuRPht0W
26
+ 4ZV7moOQYNAGG2376j/LFD5tojkJLWEsiOAd5ve/6FovkPiDfVnPpvTCsidnvqA3
27
+ nNvTV5UJOBYzM1zI8xfDRa5Mg2ggQR75fvXjTo2Jo/FDx+PN70brXJM/1zuH0Znh
28
+ z7Y03uS8cbkLiXgtp/8x9KIRASG/WFMMNtG/YmPcufCGhHlAELI6iKLdcQ5+8dHk
29
+ pmDiRWixBnkyNXu152TZBLvGUHtOAzw3aTZsir+CCdlNeNNqEW1FV21WAsYzDT2M
30
+ TsCr/azaguNozPlapt+nORWMyMR1jr7zcE2XBfmMZzLzf5t7V8RC2UoeSy09fFjT
31
+ vA6swXwe32eKYfeNJSvjiajlqXhZUrQJFBCHhOBgKQxFvU4OzVpPy8q7yszRCtg=
32
+ -----END CERTIFICATE-----