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,1023 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import threading
|
|
4
|
+
import socket
|
|
5
|
+
import os
|
|
6
|
+
import io
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
# os.system('clear')
|
|
13
|
+
|
|
14
|
+
from concurrent import futures
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
# Remove warnings
|
|
18
|
+
# import os
|
|
19
|
+
# os.environ.setdefault("GRPC_VERBOSITY", "ERROR")
|
|
20
|
+
# os.environ.setdefault("GRPC_TRACE", "")
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import grpc
|
|
29
|
+
|
|
30
|
+
import traceback
|
|
31
|
+
import contextlib
|
|
32
|
+
|
|
33
|
+
from ..common.grpc import *
|
|
34
|
+
from .reservation import reservation_handler
|
|
35
|
+
from .rpc_manager import rpc_manager
|
|
36
|
+
from .acc_perms import perms
|
|
37
|
+
from ..common.utils import *
|
|
38
|
+
from .device_manager import get_all_devices, get_all_devices_str
|
|
39
|
+
from .user_group_cli import make_user_group_local, edit_user_group_local, list_user_groups_local, remove_user_group_local, make_enrollment_codes_local, remove_enrollment_code_local, list_enrollment_codes_local, export_user_groups_csv_local, export_enrollment_codes_csv_local
|
|
40
|
+
from .user_group_handler import user_group_handler, start_usergroup_cleanup_loop
|
|
41
|
+
|
|
42
|
+
from ..host import host_tunnel_server as hts
|
|
43
|
+
from ..host.host_tunnel_server import HostTunnelRegistry, HostTunnelServicer
|
|
44
|
+
from ..common.grpc import grpc_host_pb2_grpc as host_tunnel_pb2_grpc
|
|
45
|
+
from ..common.grpc import grpc_host_pb2 as host_tunnel_pb2
|
|
46
|
+
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from prompt_toolkit import PromptSession
|
|
49
|
+
|
|
50
|
+
from ..tools.gist_status import start_status_publisher
|
|
51
|
+
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
def _xdg_config_home() -> Path:
|
|
55
|
+
return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
56
|
+
|
|
57
|
+
def _cfg_dir() -> Path:
|
|
58
|
+
# same convention as your serverrf CLI
|
|
59
|
+
return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
|
|
60
|
+
|
|
61
|
+
def _server_env_path() -> Path:
|
|
62
|
+
return _cfg_dir() / "server.env"
|
|
63
|
+
|
|
64
|
+
def _read_env_kv(path: Path) -> dict[str, str]:
|
|
65
|
+
out: dict[str, str] = {}
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return out
|
|
68
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
69
|
+
line = raw.strip()
|
|
70
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
71
|
+
continue
|
|
72
|
+
k, v = line.split("=", 1)
|
|
73
|
+
out[k.strip()] = v.strip().strip('"').strip("'")
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
def _get_port(name: str, *, default: int | None = None) -> int:
|
|
77
|
+
# precedence: real env var > server.env > default
|
|
78
|
+
v = os.getenv(name)
|
|
79
|
+
if v is None:
|
|
80
|
+
kv = _read_env_kv(_server_env_path())
|
|
81
|
+
v = kv.get(name)
|
|
82
|
+
|
|
83
|
+
if v is None or str(v).strip() == "":
|
|
84
|
+
if default is None:
|
|
85
|
+
raise SystemExit(f"Missing {name}. Set it via serverrf --config or environment.")
|
|
86
|
+
return int(default)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
n = int(str(v).strip())
|
|
90
|
+
except Exception:
|
|
91
|
+
raise SystemExit(f"Invalid {name} (expected int): {v!r}")
|
|
92
|
+
|
|
93
|
+
if n <= 0 or n > 65535:
|
|
94
|
+
raise SystemExit(f"{name} out of range (1..65535): {n}")
|
|
95
|
+
return n
|
|
96
|
+
|
|
97
|
+
session = PromptSession()
|
|
98
|
+
|
|
99
|
+
from dotenv import load_dotenv, find_dotenv
|
|
100
|
+
dotenv_path = find_dotenv('.env.server')
|
|
101
|
+
load_dotenv(dotenv_path)
|
|
102
|
+
|
|
103
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
104
|
+
s.connect(("8.8.8.8", 80))
|
|
105
|
+
|
|
106
|
+
local_ip = s.getsockname()[0] # grab the local ip
|
|
107
|
+
# local_port = os.getenv('GRPC_PORT')
|
|
108
|
+
local_port = str(_get_port("GRPC_PORT"))
|
|
109
|
+
cert_port = str(_get_port("CERT_PORT"))
|
|
110
|
+
|
|
111
|
+
# keep dotenv for *other* settings if you want, but ports come from server.env
|
|
112
|
+
dotenv_path = find_dotenv('.env.server')
|
|
113
|
+
load_dotenv(dotenv_path)
|
|
114
|
+
|
|
115
|
+
# print(f"Local IP: {local_ip}")
|
|
116
|
+
# print(f"Local Port: {local_port}")
|
|
117
|
+
|
|
118
|
+
# Implement the GenericRPC service
|
|
119
|
+
class GenericRPCServicer(grpc_pb2_grpc.GenericRPCServicer):
|
|
120
|
+
def Call(self, request, context):
|
|
121
|
+
|
|
122
|
+
# Remote Admin
|
|
123
|
+
# function_name_args = request.function_name.split(':')
|
|
124
|
+
# if function_name_args[0] == "RemoteAdmin":
|
|
125
|
+
# ok, err, caller = verify_remote_admin(request.args)
|
|
126
|
+
# if not ok:
|
|
127
|
+
# # You can also: context.abort(grpc.StatusCode.PERMISSION_DENIED, err)
|
|
128
|
+
# return grpc_pb2.GenericRPCResponse(results={
|
|
129
|
+
# "Ok": map_arg(False),
|
|
130
|
+
# "Error": map_arg(err),
|
|
131
|
+
# })
|
|
132
|
+
|
|
133
|
+
# return grpc_pb2.GenericRPCResponse(
|
|
134
|
+
# results=remote_admin_call(function_name=function_name_args[1], args=request.args)
|
|
135
|
+
# )
|
|
136
|
+
|
|
137
|
+
# Normal RPC
|
|
138
|
+
response = rpc_manager.run_rpc(function_name=request.function_name, args=request.args)
|
|
139
|
+
return grpc_pb2.GenericRPCResponse(results=response)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Function to start the server
|
|
143
|
+
def serve():
|
|
144
|
+
|
|
145
|
+
options = [
|
|
146
|
+
('grpc.max_send_message_length', 100 * 1024 * 1024),
|
|
147
|
+
('grpc.max_receive_message_length', 100 * 1024 * 1024)
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
server = grpc.server(futures.ThreadPoolExecutor(max_workers=100), options=options)
|
|
151
|
+
grpc_pb2_grpc.add_GenericRPCServicer_to_server(GenericRPCServicer(), server)
|
|
152
|
+
|
|
153
|
+
# For generating server credentials for secure connections
|
|
154
|
+
|
|
155
|
+
# Rasp
|
|
156
|
+
#* Server credentials generation: Add server ip to this line
|
|
157
|
+
#* openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.169" -addext "subjectAltName=IP:192.168.1.169"
|
|
158
|
+
|
|
159
|
+
#ubuntu VM
|
|
160
|
+
# openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.64.5" -addext "subjectAltName=IP:192.168.64.5"
|
|
161
|
+
|
|
162
|
+
#router
|
|
163
|
+
# openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=128.97.88.21" -addext "subjectAltName=IP:128.97.88.21"
|
|
164
|
+
|
|
165
|
+
#magikarp
|
|
166
|
+
# openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.109" -addext "subjectAltName=IP:192.168.1.109"
|
|
167
|
+
|
|
168
|
+
#wlab
|
|
169
|
+
# openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=164.67.195.207" -addext "subjectAltName=IP:164.67.195.207"
|
|
170
|
+
|
|
171
|
+
# Read key and certificate
|
|
172
|
+
|
|
173
|
+
# cert_key = Path(__file__).resolve().parent.parent/'certs'/'server.key'
|
|
174
|
+
# cert_crt = Path(__file__).resolve().parent.parent/'certs'/'server.crt'
|
|
175
|
+
|
|
176
|
+
# with cert_key.open('rb') as f:
|
|
177
|
+
# private_key = f.read()
|
|
178
|
+
# with cert_crt.open('rb') as f:
|
|
179
|
+
# certificate_chain = f.read()
|
|
180
|
+
|
|
181
|
+
# --- TLS certs live in user config (~/.config/remoterf/certs by default) ---
|
|
182
|
+
xdg_config = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
183
|
+
cert_dir = Path(os.getenv("REMOTERF_CERT_DIR", xdg_config / "remoterf" / "certs"))
|
|
184
|
+
|
|
185
|
+
cert_key = cert_dir / "server.key"
|
|
186
|
+
cert_crt = cert_dir / "server.crt"
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
if not cert_key.exists() or not cert_crt.exists():
|
|
190
|
+
missing = []
|
|
191
|
+
if not cert_key.exists(): missing.append(str(cert_key))
|
|
192
|
+
if not cert_crt.exists(): missing.append(str(cert_crt))
|
|
193
|
+
raise FileNotFoundError("Missing TLS file(s):\n " + "\n ".join(missing))
|
|
194
|
+
|
|
195
|
+
with cert_key.open("rb") as f:
|
|
196
|
+
private_key = f.read()
|
|
197
|
+
with cert_crt.open("rb") as f:
|
|
198
|
+
certificate_chain = f.read()
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
printf("TLS certs/keys not found or unreadable.", Sty.RED)
|
|
202
|
+
printf("Expected in: ", Sty.DEFAULT, f"{cert_dir}", Sty.MAGENTA)
|
|
203
|
+
printf("Fix: run ", Sty.DEFAULT, "RRRFcerts --ip ", Sty.GREEN, f"{local_ip}", Sty.MAGENTA, " --force", Sty.DEFAULT)
|
|
204
|
+
printf("Or set REMOTERF_CERT_DIR to your cert folder.", Sty.GRAY)
|
|
205
|
+
print(f"\nDetails: {e}\n", file=sys.stderr)
|
|
206
|
+
raise SystemExit(2)
|
|
207
|
+
|
|
208
|
+
# Create server credentials
|
|
209
|
+
server_credentials = grpc.ssl_server_credentials(
|
|
210
|
+
((private_key, certificate_chain,),))
|
|
211
|
+
|
|
212
|
+
reg = hts.get_tunnel_registry()
|
|
213
|
+
assert reg is not None
|
|
214
|
+
|
|
215
|
+
# publish registry where device_manager expects it
|
|
216
|
+
hts.TUNNEL_REGISTRY = reg
|
|
217
|
+
|
|
218
|
+
host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(
|
|
219
|
+
HostTunnelServicer(reg),
|
|
220
|
+
server
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Inject registry into rpc_manager so it can forward
|
|
224
|
+
# rpc_manager.set_tunnel_registry(registry)
|
|
225
|
+
|
|
226
|
+
# Cert fetcher
|
|
227
|
+
from .cert_provider import start_cert_provider
|
|
228
|
+
cert_server, cert_thread = start_cert_provider(host=local_ip, port=int(cert_port))
|
|
229
|
+
|
|
230
|
+
start_status_publisher()
|
|
231
|
+
|
|
232
|
+
# Add secure port
|
|
233
|
+
server.add_secure_port(f'{local_ip}:{local_port}', server_credentials)
|
|
234
|
+
server.start()
|
|
235
|
+
server.wait_for_termination()
|
|
236
|
+
|
|
237
|
+
def update_devices():
|
|
238
|
+
while True:
|
|
239
|
+
reservation_handler.update_devices()
|
|
240
|
+
time.sleep(60)
|
|
241
|
+
|
|
242
|
+
threading.Thread(target=serve, daemon=True).start()
|
|
243
|
+
threading.Thread(target=update_devices, daemon=True).start()
|
|
244
|
+
start_usergroup_cleanup_loop(interval_sec=60)
|
|
245
|
+
|
|
246
|
+
start_time = datetime.now()
|
|
247
|
+
|
|
248
|
+
def welcome():
|
|
249
|
+
printf(f"Server running nominally.", Sty.BOLD)
|
|
250
|
+
printf(f"Local IP: ", Sty.DEFAULT, f"{local_ip}", Sty.ITALIC, f" | Port: ", Sty.DEFAULT, f"{local_port}", Sty.ITALIC)
|
|
251
|
+
# printf(f" -> Started at ", Sty.DEFAULT, f"{start_time}", Sty.DEFAULT)
|
|
252
|
+
printf("Input ", Sty.DEFAULT, "'help'", Sty.BOLD," for help menu.", Sty.DEFAULT)
|
|
253
|
+
|
|
254
|
+
def clear():
|
|
255
|
+
os.system('clear')
|
|
256
|
+
welcome()
|
|
257
|
+
|
|
258
|
+
def set_acc():
|
|
259
|
+
while True:
|
|
260
|
+
username = session.prompt("Enter username to manage (c to cancel): ")
|
|
261
|
+
if username == 'exit' or username == 'c' or username == 'e':
|
|
262
|
+
return
|
|
263
|
+
perm = perms.get_perms(username)
|
|
264
|
+
if perm == []:
|
|
265
|
+
printf(f"User: {username} does not exist.", Sty.RED)
|
|
266
|
+
else:
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
print(f"User: {username} : <{perm[0][0]}>")
|
|
270
|
+
printf(f"'s'", Sty.MAGENTA, " - Set perm details", Sty.DEFAULT)
|
|
271
|
+
printf(f"'D'", Sty.MAGENTA, " - Delete user", Sty.DEFAULT)
|
|
272
|
+
printf(f"'r'", Sty.MAGENTA, " - Manage reservations", Sty.DEFAULT)
|
|
273
|
+
printf(f"'c'", Sty.MAGENTA, " - Cancel", Sty.DEFAULT)
|
|
274
|
+
|
|
275
|
+
inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT))
|
|
276
|
+
if inpu == 's':
|
|
277
|
+
inpu2 = session.prompt(stylize("Setting perms: ", Sty.DEFAULT, " (U{User}/P{PowerUser}/A{Admin}) ", Sty.GRAY, ": ", Sty.DEFAULT))
|
|
278
|
+
if inpu2 == 'U':
|
|
279
|
+
perms.set_user(username)
|
|
280
|
+
printf("Set to User. ", Sty.GREEN)
|
|
281
|
+
|
|
282
|
+
elif inpu2 == 'P':
|
|
283
|
+
printf("Setting PowerUser perms. ", Sty.BOLD, "Input 'c' to cancel anytime.", Sty.DEFAULT)
|
|
284
|
+
|
|
285
|
+
while True: # Allowed Devices
|
|
286
|
+
device_ids = session.prompt(stylize("Enter device ids allowed ", Sty.DEFAULT, 'int,int,int,...', Sty.GRAY, ": ", Sty.DEFAULT))
|
|
287
|
+
if device_ids == 'c':
|
|
288
|
+
return
|
|
289
|
+
try:
|
|
290
|
+
device_list = [int(x) for x in device_ids.split(',')]
|
|
291
|
+
if session.prompt(stylize(f"Confirm that this is the correct list of devices: {device_list} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
|
|
292
|
+
break
|
|
293
|
+
except ValueError:
|
|
294
|
+
printf("Invalid input. Try Again.", Sty.RED)
|
|
295
|
+
|
|
296
|
+
while True: # Max Reservations
|
|
297
|
+
max_res = session.prompt(stylize("Enter max reservations allowed ", Sty.DEFAULT, 'int', Sty.GRAY, ": ", Sty.DEFAULT))
|
|
298
|
+
if max_res == 'c':
|
|
299
|
+
return
|
|
300
|
+
try:
|
|
301
|
+
max_res = int(max_res)
|
|
302
|
+
if session.prompt(stylize(f"Confirm that this is the correct number of max reservations: {max_res} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
|
|
303
|
+
break
|
|
304
|
+
except ValueError:
|
|
305
|
+
printf("Invalid input. Try Again.", Sty.RED)
|
|
306
|
+
|
|
307
|
+
while True: # Max Reservation Time
|
|
308
|
+
max_res_time = session.prompt(stylize("Enter max length of a single reservation ", Sty.DEFAULT, 'int [seconds]', Sty.GRAY, ": ", Sty.DEFAULT))
|
|
309
|
+
if max_res_time == 'c':
|
|
310
|
+
return
|
|
311
|
+
try:
|
|
312
|
+
max_res_time = int(max_res_time)
|
|
313
|
+
if session.prompt(stylize(f"Confirm that this is the correct max reservation time in SECONDS: {max_res_time} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
|
|
314
|
+
break
|
|
315
|
+
except ValueError:
|
|
316
|
+
printf("Invalid input. Try Again.", Sty.RED)
|
|
317
|
+
|
|
318
|
+
perms.set_poweruser(username, devices_allowed=device_list, max_reservations=max_res, max_reservation_time_sec=max_res_time)
|
|
319
|
+
elif inpu2 == 'A':
|
|
320
|
+
perms.set_admin(username)
|
|
321
|
+
else:
|
|
322
|
+
printf("Invalid input. Skipping.", Sty.RED)
|
|
323
|
+
|
|
324
|
+
elif inpu == 'D':
|
|
325
|
+
if input(f"Are you sure you want to delete user: {username}? (Y/N): ") == 'Y':
|
|
326
|
+
printf(f"Deleting user: {username}", Sty.MAGENTA)
|
|
327
|
+
reservation_handler.remove_user(username=username)
|
|
328
|
+
else:
|
|
329
|
+
printf("Delete canceled", Sty.GRAY)
|
|
330
|
+
elif inpu == 'r':
|
|
331
|
+
print("Managing reservations:")
|
|
332
|
+
while True:
|
|
333
|
+
inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT, "'a' to add, 'c' to cancel, 'e' to exit (anytime), 'v' to view. : ", Sty.GRAY))
|
|
334
|
+
if inpu == 'e':
|
|
335
|
+
break
|
|
336
|
+
elif inpu == 'a':
|
|
337
|
+
while True:
|
|
338
|
+
device_id = session.prompt(stylize("Enter device id: ", Sty.DEFAULT))
|
|
339
|
+
if device_id == 'c' or device_id == 'e':
|
|
340
|
+
break
|
|
341
|
+
try:
|
|
342
|
+
device_id = int(device_id)
|
|
343
|
+
if device_id not in get_all_devices():
|
|
344
|
+
printf("Invalid device id.", Sty.GRAY)
|
|
345
|
+
continue
|
|
346
|
+
start_time = session.prompt(stylize("Enter start time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
|
|
347
|
+
end_time = session.prompt(stylize("Enter end time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
|
|
348
|
+
token = reservation_handler.reserve_device(username=username, device_id=device_id, start_time=datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S'), end_time=datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S'), is_server=True)
|
|
349
|
+
api = unmap_arg(token['Token'])
|
|
350
|
+
printf("Reservation added. TOKEN -> ", Sty.GREEN, f"{api}", Sty.MAGENTA)
|
|
351
|
+
break
|
|
352
|
+
except ValueError:
|
|
353
|
+
printf("Invalid input. Try Again.", Sty.GRAY)
|
|
354
|
+
elif inpu == 'v':
|
|
355
|
+
reservations = reservation_handler.grab_all_reservations_server()
|
|
356
|
+
for key, reservation in reservations.items():
|
|
357
|
+
if reservation[0] == username:
|
|
358
|
+
print(f"ID: {key}, User: {reservation[0]}, Device: {reservation[1]}, Start: {reservation[2]}, End: {reservation[3]}")
|
|
359
|
+
if reservations == {}:
|
|
360
|
+
printf("No reservations found.", Sty.GRAY)
|
|
361
|
+
elif inpu == 'c':
|
|
362
|
+
while True:
|
|
363
|
+
try:
|
|
364
|
+
inpu2 = session.prompt(stylize("Enter reservation id to cancel: ", Sty.DEFAULT))
|
|
365
|
+
if inpu2 == 'c' or inpu2 == 'e':
|
|
366
|
+
break
|
|
367
|
+
reservation_handler.remove_reservation(res_id=int(inpu2))
|
|
368
|
+
printf(f"Reservation {inpu2} removed.", Sty.GREEN)
|
|
369
|
+
break
|
|
370
|
+
except Exception as e:
|
|
371
|
+
printf(f"Error: {e}", Sty.GRAY)
|
|
372
|
+
|
|
373
|
+
elif inpu == 'c':
|
|
374
|
+
print("Canceled. Exiting ...")
|
|
375
|
+
|
|
376
|
+
def get_devices():
|
|
377
|
+
data = get_all_devices_str()
|
|
378
|
+
|
|
379
|
+
printf("Devices:", Sty.BOLD)
|
|
380
|
+
for key, value in data.items():
|
|
381
|
+
printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{(value)}", Sty.GRAY)
|
|
382
|
+
|
|
383
|
+
def _fmt_ms_ago(ms: int) -> str:
|
|
384
|
+
if not ms:
|
|
385
|
+
return "-"
|
|
386
|
+
now = int(time.time() * 1000)
|
|
387
|
+
dt = max(0, now - int(ms))
|
|
388
|
+
if dt < 1000:
|
|
389
|
+
return f"{dt}ms"
|
|
390
|
+
if dt < 60_000:
|
|
391
|
+
return f"{dt/1000:.1f}s"
|
|
392
|
+
return f"{dt/60_000:.1f}m"
|
|
393
|
+
|
|
394
|
+
def _cmd_hosts_status() -> None:
|
|
395
|
+
reg = hts.get_tunnel_registry()
|
|
396
|
+
assert reg is not None
|
|
397
|
+
|
|
398
|
+
# print("\n[hosts status] registry debug")
|
|
399
|
+
# print(f" hts module: {hts.__name__}")
|
|
400
|
+
# reg = hts.get_tunnel_registry(create=False)
|
|
401
|
+
# print(f" registry id: {id(reg) if reg else None}\n")
|
|
402
|
+
|
|
403
|
+
hosts = reg.list_hosts() # host_id -> HostStatus
|
|
404
|
+
devices = reg.list_devices() # device_id -> DeviceStatus
|
|
405
|
+
|
|
406
|
+
# Build host -> device list
|
|
407
|
+
by_host: dict[str, list[str]] = {}
|
|
408
|
+
for did, ds in devices.items():
|
|
409
|
+
by_host.setdefault(ds.host_id or "-", []).append(did)
|
|
410
|
+
|
|
411
|
+
printf("Hosts:", Sty.BOLD)
|
|
412
|
+
if not hosts:
|
|
413
|
+
printf(" (none)", Sty.GRAY)
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
for hid, hs in sorted(hosts.items(), key=lambda kv: kv[0]):
|
|
417
|
+
dlist = sorted(by_host.get(hid, []))
|
|
418
|
+
printf(
|
|
419
|
+
" ", Sty.DEFAULT,
|
|
420
|
+
f"{hid}", Sty.MAGENTA,
|
|
421
|
+
" online=", Sty.DEFAULT, f"{hs.online}", Sty.GRAY if hs.online else Sty.GRAY,
|
|
422
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(hs.last_seen_ms)} ago", Sty.GRAY,
|
|
423
|
+
" devices=", Sty.DEFAULT, f"{len(dlist)}", Sty.GRAY
|
|
424
|
+
)
|
|
425
|
+
for did in dlist:
|
|
426
|
+
ds = devices.get(did)
|
|
427
|
+
if ds is None:
|
|
428
|
+
continue
|
|
429
|
+
printf(
|
|
430
|
+
" - ", Sty.DEFAULT,
|
|
431
|
+
f"{did}", Sty.MAGENTA,
|
|
432
|
+
" local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
|
|
433
|
+
" online=", Sty.DEFAULT, f"{ds.online}", Sty.GRAY if ds.online else Sty.GRAY,
|
|
434
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.GRAY
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def _cmd_devices_status() -> None:
|
|
438
|
+
reg = hts.get_tunnel_registry()
|
|
439
|
+
assert reg is not None
|
|
440
|
+
|
|
441
|
+
devices = reg.list_devices()
|
|
442
|
+
printf("Devices (live):", Sty.BOLD)
|
|
443
|
+
if not devices:
|
|
444
|
+
printf(" (none)", Sty.GRAY)
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
for did, ds in sorted(devices.items(), key=lambda kv: kv[0]):
|
|
448
|
+
printf(
|
|
449
|
+
" ", Sty.DEFAULT,
|
|
450
|
+
f"{did}", Sty.MAGENTA,
|
|
451
|
+
" host=", Sty.DEFAULT, f"{ds.host_id or '-'}", Sty.GRAY,
|
|
452
|
+
" local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
|
|
453
|
+
" online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
|
|
454
|
+
" last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
from typing import Iterable
|
|
458
|
+
import contextlib
|
|
459
|
+
|
|
460
|
+
def _repo_root_guess() -> Path:
|
|
461
|
+
"""
|
|
462
|
+
Best-effort: find <repo>/src/... and return <repo>.
|
|
463
|
+
Fallback: walk up a few levels from this file.
|
|
464
|
+
"""
|
|
465
|
+
p = Path(__file__).resolve()
|
|
466
|
+
for parent in p.parents:
|
|
467
|
+
if parent.name == "src":
|
|
468
|
+
return parent.parent
|
|
469
|
+
# fallback
|
|
470
|
+
return p.parents[3] if len(p.parents) >= 4 else p.parent
|
|
471
|
+
|
|
472
|
+
# Optional: if you later add "kick" support (see section 2)
|
|
473
|
+
HOST_TUNNEL_EPOCH = 0
|
|
474
|
+
|
|
475
|
+
def _read_env_kv_file(path: Path) -> dict[str, str]:
|
|
476
|
+
out: dict[str, str] = {}
|
|
477
|
+
if not path.exists():
|
|
478
|
+
return out
|
|
479
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
480
|
+
line = raw.strip()
|
|
481
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
482
|
+
continue
|
|
483
|
+
k, v = line.split("=", 1)
|
|
484
|
+
out[k.strip()] = v.strip().strip('"').strip("'")
|
|
485
|
+
return out
|
|
486
|
+
|
|
487
|
+
def _env_values_as_paths(env_path: Path) -> list[Path]:
|
|
488
|
+
"""
|
|
489
|
+
Parse an env file and treat values that look like filesystem paths as candidate dirs.
|
|
490
|
+
Relative paths are resolved relative to the env file's directory.
|
|
491
|
+
"""
|
|
492
|
+
kv = _read_env_kv_file(env_path)
|
|
493
|
+
base = env_path.parent
|
|
494
|
+
out: list[Path] = []
|
|
495
|
+
|
|
496
|
+
for _, v in kv.items():
|
|
497
|
+
if not v:
|
|
498
|
+
continue
|
|
499
|
+
# heuristics: looks like a path
|
|
500
|
+
if ("/" not in v) and ("\\" not in v) and (not v.startswith(".")) and (not v.startswith("~")):
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
p = Path(v).expanduser()
|
|
504
|
+
if not p.is_absolute():
|
|
505
|
+
p = (base / p).resolve()
|
|
506
|
+
out.append(p)
|
|
507
|
+
|
|
508
|
+
return out
|
|
509
|
+
|
|
510
|
+
def _host_state_dirs() -> list[Path]:
|
|
511
|
+
"""
|
|
512
|
+
Aggressive candidate list:
|
|
513
|
+
- user config (~/.config/remoterf or REMOTERF_CONFIG_DIR)
|
|
514
|
+
- repo/.config
|
|
515
|
+
- repo/config (+ common subdirs)
|
|
516
|
+
- src/remoteRF_server/config
|
|
517
|
+
- plus env overrides + paths referenced by .env.host_directory (if present)
|
|
518
|
+
"""
|
|
519
|
+
repo = _repo_root_guess()
|
|
520
|
+
|
|
521
|
+
cands: list[Path] = [
|
|
522
|
+
_cfg_dir(),
|
|
523
|
+
repo / ".config",
|
|
524
|
+
repo / "config",
|
|
525
|
+
repo / "config" / "remoteRF_server",
|
|
526
|
+
repo / "config" / "remoteRF_host",
|
|
527
|
+
repo / "src" / "remoteRF_server" / "config",
|
|
528
|
+
(Path(__file__).resolve().parent.parent / "config"),
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
# Environment overrides (add any you might plausibly use)
|
|
532
|
+
for k in (
|
|
533
|
+
"REMOTERF_HOSTDIR_DIR",
|
|
534
|
+
"REMOTERF_HOST_DIRECTORY_DIR",
|
|
535
|
+
"REMOTERF_HOST_DIR",
|
|
536
|
+
"HOST_DIRECTORY_DIR",
|
|
537
|
+
"HOST_DIRECTORY_PATH",
|
|
538
|
+
"REMOTERF_STATE_DIR",
|
|
539
|
+
):
|
|
540
|
+
v = os.getenv(k)
|
|
541
|
+
if v:
|
|
542
|
+
cands.append(Path(v).expanduser())
|
|
543
|
+
|
|
544
|
+
# If you have a .env.host_directory file, harvest any path-like values from it
|
|
545
|
+
env_host_dir = repo / "src" / "remoteRF_server" / "config" / ".env.host_directory"
|
|
546
|
+
if env_host_dir.exists():
|
|
547
|
+
cands.extend(_env_values_as_paths(env_host_dir))
|
|
548
|
+
|
|
549
|
+
# de-dupe + keep only dirs (or dirs that could exist)
|
|
550
|
+
out: list[Path] = []
|
|
551
|
+
seen: set[str] = set()
|
|
552
|
+
for d in cands:
|
|
553
|
+
try:
|
|
554
|
+
rp = str(d.expanduser().resolve())
|
|
555
|
+
except Exception:
|
|
556
|
+
rp = str(d)
|
|
557
|
+
if rp not in seen:
|
|
558
|
+
seen.add(rp)
|
|
559
|
+
out.append(d)
|
|
560
|
+
return out
|
|
561
|
+
|
|
562
|
+
def _host_state_files() -> list[Path]:
|
|
563
|
+
"""
|
|
564
|
+
Broader scan than just host_directory.env:
|
|
565
|
+
We only match filenames that clearly look like host/tunnel/registry state.
|
|
566
|
+
"""
|
|
567
|
+
patterns = [
|
|
568
|
+
"host_directory*.env",
|
|
569
|
+
"host_directory*.json",
|
|
570
|
+
"host_directory*.db",
|
|
571
|
+
"host_directory*.sqlite*",
|
|
572
|
+
"host_directory*.pkl",
|
|
573
|
+
"host_tunnel*.env",
|
|
574
|
+
"host_tunnel*.json",
|
|
575
|
+
"tunnel_registry*.env",
|
|
576
|
+
"tunnel_registry*.json",
|
|
577
|
+
"*host*registry*.json",
|
|
578
|
+
"*host*registry*.db",
|
|
579
|
+
"*host*registry*.sqlite*",
|
|
580
|
+
"*host*state*.json",
|
|
581
|
+
"*host*state*.db",
|
|
582
|
+
"*host*state*.sqlite*",
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
files: list[Path] = []
|
|
586
|
+
for d in _host_state_dirs():
|
|
587
|
+
try:
|
|
588
|
+
if not d.exists() or not d.is_dir():
|
|
589
|
+
continue
|
|
590
|
+
except Exception:
|
|
591
|
+
continue
|
|
592
|
+
|
|
593
|
+
for pat in patterns:
|
|
594
|
+
try:
|
|
595
|
+
files.extend(sorted(d.glob(pat)))
|
|
596
|
+
except Exception:
|
|
597
|
+
pass
|
|
598
|
+
|
|
599
|
+
# de-dupe
|
|
600
|
+
out: list[Path] = []
|
|
601
|
+
seen: set[str] = set()
|
|
602
|
+
for p in files:
|
|
603
|
+
try:
|
|
604
|
+
rp = str(p.resolve())
|
|
605
|
+
except Exception:
|
|
606
|
+
rp = str(p)
|
|
607
|
+
if rp not in seen:
|
|
608
|
+
seen.add(rp)
|
|
609
|
+
out.append(p)
|
|
610
|
+
return out
|
|
611
|
+
|
|
612
|
+
def _deep_clear_state(obj: Any) -> list[str]:
|
|
613
|
+
"""
|
|
614
|
+
Best-effort: clear dict/list/set fields on an object, recursively,
|
|
615
|
+
but only for fields that look like state (host/device/route/session/cache/store/etc).
|
|
616
|
+
"""
|
|
617
|
+
cleared: list[str] = []
|
|
618
|
+
seen: set[int] = set()
|
|
619
|
+
|
|
620
|
+
state_name = re.compile(r"(host|device|route|session|dir|store|cache|meta|seen|status)", re.I)
|
|
621
|
+
|
|
622
|
+
def walk(x: Any, prefix: str) -> None:
|
|
623
|
+
oid = id(x)
|
|
624
|
+
if oid in seen:
|
|
625
|
+
return
|
|
626
|
+
seen.add(oid)
|
|
627
|
+
|
|
628
|
+
# containers
|
|
629
|
+
if isinstance(x, dict):
|
|
630
|
+
x.clear()
|
|
631
|
+
cleared.append(prefix)
|
|
632
|
+
return
|
|
633
|
+
if isinstance(x, list):
|
|
634
|
+
x.clear()
|
|
635
|
+
cleared.append(prefix)
|
|
636
|
+
return
|
|
637
|
+
if isinstance(x, set):
|
|
638
|
+
x.clear()
|
|
639
|
+
cleared.append(prefix)
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
# objects with __dict__
|
|
643
|
+
d = getattr(x, "__dict__", None)
|
|
644
|
+
if not isinstance(d, dict):
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
for k, v in list(d.items()):
|
|
648
|
+
# never touch locks/threads/etc
|
|
649
|
+
if k in ("_lock", "lock", "_thread", "thread", "_executor", "executor"):
|
|
650
|
+
continue
|
|
651
|
+
if not state_name.search(k):
|
|
652
|
+
continue
|
|
653
|
+
|
|
654
|
+
if isinstance(v, (dict, list, set)):
|
|
655
|
+
try:
|
|
656
|
+
v.clear()
|
|
657
|
+
cleared.append(f"{prefix}.{k}")
|
|
658
|
+
except Exception:
|
|
659
|
+
pass
|
|
660
|
+
else:
|
|
661
|
+
# recurse into nested state objects (EnvStore, etc.)
|
|
662
|
+
walk(v, f"{prefix}.{k}")
|
|
663
|
+
|
|
664
|
+
walk(obj, obj.__class__.__name__)
|
|
665
|
+
return cleared
|
|
666
|
+
|
|
667
|
+
def _try_clear_registry_in_memory(reg: object) -> list[str]:
|
|
668
|
+
"""
|
|
669
|
+
Stronger in-memory clear:
|
|
670
|
+
- calls obvious reset/clear methods if present
|
|
671
|
+
- clears common containers
|
|
672
|
+
- recursively clears state-like fields (even if names differ)
|
|
673
|
+
"""
|
|
674
|
+
cleared: list[str] = []
|
|
675
|
+
|
|
676
|
+
lock = getattr(reg, "_lock", None)
|
|
677
|
+
acquired = False
|
|
678
|
+
try:
|
|
679
|
+
if lock is not None and hasattr(lock, "acquire") and hasattr(lock, "release"):
|
|
680
|
+
lock.acquire()
|
|
681
|
+
acquired = True
|
|
682
|
+
|
|
683
|
+
# Prefer explicit APIs if you have them
|
|
684
|
+
for meth in (
|
|
685
|
+
"clear",
|
|
686
|
+
"reset",
|
|
687
|
+
"wipe",
|
|
688
|
+
"wipe_all",
|
|
689
|
+
"wipe_state",
|
|
690
|
+
"clear_state",
|
|
691
|
+
"reset_state",
|
|
692
|
+
"prune",
|
|
693
|
+
"gc",
|
|
694
|
+
):
|
|
695
|
+
fn = getattr(reg, meth, None)
|
|
696
|
+
if callable(fn):
|
|
697
|
+
try:
|
|
698
|
+
fn()
|
|
699
|
+
cleared.append(f"{meth}()")
|
|
700
|
+
except Exception:
|
|
701
|
+
pass
|
|
702
|
+
|
|
703
|
+
# Clear obvious containers by direct attribute scan
|
|
704
|
+
for attr, v in list(getattr(reg, "__dict__", {}).items()):
|
|
705
|
+
if isinstance(v, (dict, list, set)):
|
|
706
|
+
try:
|
|
707
|
+
v.clear()
|
|
708
|
+
cleared.append(attr)
|
|
709
|
+
except Exception:
|
|
710
|
+
pass
|
|
711
|
+
|
|
712
|
+
# Recursive targeted clear (for EnvStore etc.)
|
|
713
|
+
cleared.extend(_deep_clear_state(reg))
|
|
714
|
+
|
|
715
|
+
finally:
|
|
716
|
+
if acquired:
|
|
717
|
+
try:
|
|
718
|
+
lock.release()
|
|
719
|
+
except Exception:
|
|
720
|
+
pass
|
|
721
|
+
|
|
722
|
+
# de-dupe
|
|
723
|
+
out: list[str] = []
|
|
724
|
+
seen: set[str] = set()
|
|
725
|
+
for x in cleared:
|
|
726
|
+
if x not in seen:
|
|
727
|
+
seen.add(x)
|
|
728
|
+
out.append(x)
|
|
729
|
+
return out
|
|
730
|
+
|
|
731
|
+
def _cmd_hosts_wipe(*, yes: bool = False) -> None:
|
|
732
|
+
if not yes:
|
|
733
|
+
try:
|
|
734
|
+
resp = session.prompt(stylize("Type 'wipe-hosts' to confirm wiping host state: ", Sty.RED))
|
|
735
|
+
except KeyboardInterrupt:
|
|
736
|
+
printf("Cancelled.", Sty.GRAY)
|
|
737
|
+
return
|
|
738
|
+
if (resp or "").strip().lower() != "wipe-hosts":
|
|
739
|
+
printf("Wipe aborted.", Sty.GRAY)
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
# 1) delete persisted files (if any)
|
|
743
|
+
files = [p for p in _host_state_files() if p.exists()]
|
|
744
|
+
if not files:
|
|
745
|
+
printf("No persisted host directory files found (searched common dirs/patterns).", Sty.YELLOW)
|
|
746
|
+
else:
|
|
747
|
+
for p in files:
|
|
748
|
+
try:
|
|
749
|
+
p.unlink()
|
|
750
|
+
printf("Deleted: ", Sty.DEFAULT, f"{p}", Sty.MAGENTA)
|
|
751
|
+
except Exception as e:
|
|
752
|
+
printf("ERROR deleting: ", Sty.RED, f"{p} -> {e}", Sty.DEFAULT)
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
# 2) clear in-memory registry (THIS is what drives `hosts status`)
|
|
756
|
+
reg = hts.get_tunnel_registry()
|
|
757
|
+
if reg is None:
|
|
758
|
+
printf("Host tunnel registry not initialized; only persisted wipe attempted.", Sty.GRAY)
|
|
759
|
+
else:
|
|
760
|
+
cleared = _try_clear_registry_in_memory(reg)
|
|
761
|
+
if cleared:
|
|
762
|
+
printf("Cleared in-memory registry state:", Sty.GREEN)
|
|
763
|
+
# keep output short-ish
|
|
764
|
+
for item in cleared[:20]:
|
|
765
|
+
printf(" - ", Sty.DEFAULT, item, Sty.GRAY)
|
|
766
|
+
if len(cleared) > 20:
|
|
767
|
+
printf(" ...", Sty.GRAY)
|
|
768
|
+
else:
|
|
769
|
+
printf("WARNING: in-memory wipe could not confirm any cleared fields.", Sty.YELLOW)
|
|
770
|
+
printf("If `hosts status` still shows data, it is almost certainly being repopulated by live connections.", Sty.YELLOW)
|
|
771
|
+
|
|
772
|
+
printf("Host wipe complete.", Sty.GREEN)
|
|
773
|
+
|
|
774
|
+
# def help():
|
|
775
|
+
# printf("/------- HELP -------/", Sty.BOLD)
|
|
776
|
+
# printf("'exit' ", Sty.MAGENTA, " - Stop server.", Sty.DEFAULT)
|
|
777
|
+
# printf("'help' ", Sty.MAGENTA, " - Help.", Sty.DEFAULT)
|
|
778
|
+
# printf("'clear' ", Sty.MAGENTA, " - Clear terminal screen.", Sty.DEFAULT)
|
|
779
|
+
# printf("'show a' ", Sty.MAGENTA, " - Print all accounts", Sty.DEFAULT)
|
|
780
|
+
# printf("'show d' ", Sty.MAGENTA, " - Print all devices", Sty.DEFAULT)
|
|
781
|
+
# # printf("'view p' ", Sty.MAGENTA, " - Print all perms", Sty.DEFAULT) # Deprecated
|
|
782
|
+
# printf("'show r' ", Sty.MAGENTA, " - Print all reservations", Sty.DEFAULT)
|
|
783
|
+
# printf("'rm aa' ", Sty.MAGENTA, " - Remove all accounts", Sty.DEFAULT)
|
|
784
|
+
# # printf("'rm a' ", Sty.MAGENTA, " - Remove one account", Sty.DEFAULT)
|
|
785
|
+
# printf("'rm ar' ", Sty.MAGENTA, " - Remove all reservations", Sty.DEFAULT)
|
|
786
|
+
# printf("'rm db' ", Sty.MAGENTA, " - Remove all database entries", Sty.DEFAULT)
|
|
787
|
+
# printf("'setacc' ", Sty.MAGENTA, " - Set account details (and deletion)", Sty.DEFAULT)
|
|
788
|
+
|
|
789
|
+
# printf("'mk group' ", Sty.MAGENTA, " - Make a user group", Sty.DEFAULT)
|
|
790
|
+
# printf("'rm group' ", Sty.MAGENTA, " - Remove a user group", Sty.DEFAULT)
|
|
791
|
+
# printf("'edit group' ", Sty.MAGENTA, " - Edit a user group", Sty.DEFAULT)
|
|
792
|
+
# printf("'show g' ", Sty.MAGENTA, " - View all user groups", Sty.DEFAULT)
|
|
793
|
+
|
|
794
|
+
# printf("'mk codes' ", Sty.MAGENTA, " - Make enrollment codes", Sty.DEFAULT)
|
|
795
|
+
# printf("'show c' ", Sty.MAGENTA, " - View all enrollment codes", Sty.DEFAULT)
|
|
796
|
+
# printf("'rm codes' ", Sty.MAGENTA, " - Remove enrollment code", Sty.DEFAULT)
|
|
797
|
+
|
|
798
|
+
# def server_input(inpu:str) -> bool:
|
|
799
|
+
# if inpu == "h" or inpu == "help":
|
|
800
|
+
# help()
|
|
801
|
+
# elif inpu == 'show r':
|
|
802
|
+
# reservation_handler.print_all_reservations()
|
|
803
|
+
# elif inpu == 'show a':
|
|
804
|
+
# reservation_handler.print_all_accounts()
|
|
805
|
+
# elif inpu == 'show p':
|
|
806
|
+
# perms.print_perms()
|
|
807
|
+
# elif inpu == 'show d':
|
|
808
|
+
# get_devices()
|
|
809
|
+
# elif inpu == 'clear':
|
|
810
|
+
# clear()
|
|
811
|
+
# elif inpu == 'rm aa':
|
|
812
|
+
# reservation_handler.remove_all_users()
|
|
813
|
+
# elif inpu == 'rm ar':
|
|
814
|
+
# reservation_handler.remove_all_reservations()
|
|
815
|
+
# elif inpu == 'rm db':
|
|
816
|
+
# reservation_handler.rm_db()
|
|
817
|
+
# elif inpu == 'setacc':
|
|
818
|
+
# set_acc()
|
|
819
|
+
# elif inpu in ('mk group', 'mk g'):
|
|
820
|
+
# make_user_group_local(session)
|
|
821
|
+
# elif inpu in ('rm group', 'rm g'):
|
|
822
|
+
# remove_user_group_local(session)
|
|
823
|
+
# elif inpu in ('edit group', 'edit g'):
|
|
824
|
+
# edit_user_group_local(session)
|
|
825
|
+
# elif inpu in ('show g', 'show groups'):
|
|
826
|
+
# list_user_groups_local(session)
|
|
827
|
+
# elif inpu in ('mk codes', 'mk code', 'mk c'):
|
|
828
|
+
# make_enrollment_codes_local(session)
|
|
829
|
+
# elif inpu == 'show c':
|
|
830
|
+
# list_enrollment_codes_local(session)
|
|
831
|
+
# elif inpu in ('rm code', 'rm c', 'rm codes'):
|
|
832
|
+
# remove_enrollment_code_local(session)
|
|
833
|
+
# elif inpu in ('exit', 'quit'):
|
|
834
|
+
# return False
|
|
835
|
+
# else:
|
|
836
|
+
# print(f"Invalid command: {inpu}")
|
|
837
|
+
# return True
|
|
838
|
+
|
|
839
|
+
from typing import Callable
|
|
840
|
+
|
|
841
|
+
def _norm_cmd(s: str) -> str:
|
|
842
|
+
return " ".join((s or "").strip().split()).lower()
|
|
843
|
+
|
|
844
|
+
def _cmd_status() -> None:
|
|
845
|
+
try:
|
|
846
|
+
uptime = datetime.now() - start_time
|
|
847
|
+
printf("Status:", Sty.BOLD)
|
|
848
|
+
printf(" Started: ", Sty.DEFAULT, f"{start_time}", Sty.BLUE)
|
|
849
|
+
printf(" Uptime : ", Sty.DEFAULT, f"{str(uptime).split('.')[0]}", Sty.BLUE)
|
|
850
|
+
printf(" Bind : ", Sty.DEFAULT, f"{local_ip}:{local_port}", Sty.MAGENTA)
|
|
851
|
+
printf(" Cert : ", Sty.DEFAULT, f"{local_ip}:{cert_port}", Sty.MAGENTA)
|
|
852
|
+
except Exception:
|
|
853
|
+
printf("Status unavailable.", Sty.RED)
|
|
854
|
+
|
|
855
|
+
def help() -> None:
|
|
856
|
+
printf("RemoteRF Server Shell", Sty.BOLD)
|
|
857
|
+
printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands. Type ", Sty.DEFAULT, "quit", Sty.MAGENTA, " to exit.", Sty.DEFAULT)
|
|
858
|
+
print()
|
|
859
|
+
|
|
860
|
+
printf("Server:", Sty.BOLD)
|
|
861
|
+
printf(" help, h ", Sty.MAGENTA, "- Show this help", Sty.DEFAULT)
|
|
862
|
+
printf(" clear ", Sty.MAGENTA, "- Clear the screen", Sty.DEFAULT)
|
|
863
|
+
printf(" status ", Sty.MAGENTA, "- Show server status (uptime/bind)", Sty.DEFAULT)
|
|
864
|
+
printf(" quit / exit ", Sty.MAGENTA, "- Exit the shell", Sty.DEFAULT)
|
|
865
|
+
print()
|
|
866
|
+
|
|
867
|
+
printf("Users:", Sty.BOLD)
|
|
868
|
+
printf(" users list ", Sty.MAGENTA, "- List accounts", Sty.DEFAULT)
|
|
869
|
+
printf(" users manage ", Sty.MAGENTA, "- Manage a user (perms / delete / reservations)", Sty.DEFAULT)
|
|
870
|
+
printf(" users purge ", Sty.MAGENTA, "- Remove ALL users", Sty.DEFAULT)
|
|
871
|
+
print()
|
|
872
|
+
|
|
873
|
+
printf("Devices:", Sty.BOLD)
|
|
874
|
+
printf(" devices list ", Sty.MAGENTA, "- List devices", Sty.DEFAULT)
|
|
875
|
+
printf(" devices status ", Sty.MAGENTA, "- Live device online/last_seen/route", Sty.DEFAULT)
|
|
876
|
+
print()
|
|
877
|
+
|
|
878
|
+
printf("Reservations:", Sty.BOLD)
|
|
879
|
+
printf(" reservations list ", Sty.MAGENTA, "- List reservations", Sty.DEFAULT)
|
|
880
|
+
printf(" reservations purge ", Sty.MAGENTA, "- Remove ALL reservations", Sty.DEFAULT)
|
|
881
|
+
print()
|
|
882
|
+
|
|
883
|
+
printf("Groups:", Sty.BOLD)
|
|
884
|
+
printf(" groups list ", Sty.MAGENTA, "- List user groups", Sty.DEFAULT)
|
|
885
|
+
printf(" groups create ", Sty.MAGENTA, "- Create a user group (interactive)", Sty.DEFAULT)
|
|
886
|
+
printf(" groups edit ", Sty.MAGENTA, "- Edit a user group (interactive)", Sty.DEFAULT)
|
|
887
|
+
printf(" groups delete ", Sty.MAGENTA, "- Delete a user group (interactive)", Sty.DEFAULT)
|
|
888
|
+
printf(" groups csv ", Sty.MAGENTA, "- Export ALL user groups as CSV to Downloads", Sty.DEFAULT)
|
|
889
|
+
print()
|
|
890
|
+
|
|
891
|
+
printf("Enrollment Codes:", Sty.BOLD)
|
|
892
|
+
printf(" codes list ", Sty.MAGENTA, "- List enrollment codes", Sty.DEFAULT)
|
|
893
|
+
printf(" codes create ", Sty.MAGENTA, "- Create enrollment codes (interactive)", Sty.DEFAULT)
|
|
894
|
+
printf(" codes delete ", Sty.MAGENTA, "- Delete an enrollment code (interactive)", Sty.DEFAULT)
|
|
895
|
+
printf(" codes csv ", Sty.MAGENTA, "- Export ALL enrollment codes as CSV to Downloads", Sty.DEFAULT)
|
|
896
|
+
print()
|
|
897
|
+
|
|
898
|
+
printf("Host Tunnel (live):", Sty.BOLD)
|
|
899
|
+
printf(" hosts status ", Sty.MAGENTA, "- Live host online/last_seen/devices", Sty.DEFAULT)
|
|
900
|
+
printf(" hosts wipe ", Sty.MAGENTA, "- Wipe persisted host directory state (and clear in-memory)", Sty.DEFAULT)
|
|
901
|
+
|
|
902
|
+
printf("Database:", Sty.BOLD)
|
|
903
|
+
printf(" db purge ", Sty.MAGENTA, "- Remove all database entries", Sty.DEFAULT)
|
|
904
|
+
print()
|
|
905
|
+
|
|
906
|
+
def server_input(inpu: str) -> bool:
|
|
907
|
+
cmd = _norm_cmd(inpu)
|
|
908
|
+
if not cmd:
|
|
909
|
+
return True
|
|
910
|
+
|
|
911
|
+
def _stay(fn: Callable[[], None]) -> bool:
|
|
912
|
+
fn()
|
|
913
|
+
return True
|
|
914
|
+
|
|
915
|
+
DISPATCH: dict[str, Callable[[], bool]] = {
|
|
916
|
+
# ---- server ----
|
|
917
|
+
"help": lambda: _stay(help),
|
|
918
|
+
"h": lambda: _stay(help),
|
|
919
|
+
"?": lambda: _stay(help),
|
|
920
|
+
|
|
921
|
+
"clear": lambda: _stay(clear),
|
|
922
|
+
"cls": lambda: _stay(clear),
|
|
923
|
+
|
|
924
|
+
"status": lambda: _stay(_cmd_status),
|
|
925
|
+
"server status": lambda: _stay(_cmd_status),
|
|
926
|
+
|
|
927
|
+
"quit": lambda: False,
|
|
928
|
+
"exit": lambda: False,
|
|
929
|
+
"q": lambda: False,
|
|
930
|
+
|
|
931
|
+
# ---- users ----
|
|
932
|
+
"users list": lambda: _stay(reservation_handler.print_all_accounts),
|
|
933
|
+
"u list": lambda: _stay(reservation_handler.print_all_accounts),
|
|
934
|
+
"accounts list": lambda: _stay(reservation_handler.print_all_accounts),
|
|
935
|
+
|
|
936
|
+
"users manage": lambda: _stay(set_acc),
|
|
937
|
+
"users manage": lambda: _stay(set_acc),
|
|
938
|
+
"setacc": lambda: _stay(set_acc), # old
|
|
939
|
+
|
|
940
|
+
"users purge": lambda: _stay(reservation_handler.remove_all_users),
|
|
941
|
+
"rm aa": lambda: _stay(reservation_handler.remove_all_users), # old
|
|
942
|
+
|
|
943
|
+
# perms (kept; optional to advertise)
|
|
944
|
+
"users perms": lambda: _stay(perms.print_perms),
|
|
945
|
+
"show p": lambda: _stay(perms.print_perms), # old
|
|
946
|
+
|
|
947
|
+
# ---- devices ----
|
|
948
|
+
"devices list": lambda: _stay(get_devices),
|
|
949
|
+
"show d": lambda: _stay(get_devices), # old
|
|
950
|
+
|
|
951
|
+
# ---- reservations ----
|
|
952
|
+
"reservations list": lambda: _stay(reservation_handler.print_all_reservations),
|
|
953
|
+
"show r": lambda: _stay(reservation_handler.print_all_reservations), # old
|
|
954
|
+
|
|
955
|
+
"reservations purge": lambda: _stay(reservation_handler.remove_all_reservations),
|
|
956
|
+
"rm ar": lambda: _stay(reservation_handler.remove_all_reservations), # old
|
|
957
|
+
|
|
958
|
+
# ---- db ----
|
|
959
|
+
"db purge": lambda: _stay(reservation_handler.rm_db),
|
|
960
|
+
"rm db": lambda: _stay(reservation_handler.rm_db), # old
|
|
961
|
+
|
|
962
|
+
# ---- groups ----
|
|
963
|
+
"groups create": lambda: _stay(lambda: make_user_group_local(session)),
|
|
964
|
+
"mk group": lambda: _stay(lambda: make_user_group_local(session)), # old
|
|
965
|
+
"mk g": lambda: _stay(lambda: make_user_group_local(session)), # old
|
|
966
|
+
|
|
967
|
+
"groups delete": lambda: _stay(lambda: remove_user_group_local(session)),
|
|
968
|
+
"rm group": lambda: _stay(lambda: remove_user_group_local(session)), # old
|
|
969
|
+
"rm g": lambda: _stay(lambda: remove_user_group_local(session)), # old
|
|
970
|
+
|
|
971
|
+
"groups edit": lambda: _stay(lambda: edit_user_group_local(session)),
|
|
972
|
+
"edit group": lambda: _stay(lambda: edit_user_group_local(session)), # old
|
|
973
|
+
"edit g": lambda: _stay(lambda: edit_user_group_local(session)), # old
|
|
974
|
+
|
|
975
|
+
"groups list": lambda: _stay(lambda: list_user_groups_local(session)),
|
|
976
|
+
"show g": lambda: _stay(lambda: list_user_groups_local(session)), # old
|
|
977
|
+
"show groups": lambda: _stay(lambda: list_user_groups_local(session)), # old
|
|
978
|
+
"groups csv": lambda: _stay(export_user_groups_csv_local),
|
|
979
|
+
|
|
980
|
+
# ---- enrollment codes ----
|
|
981
|
+
"codes create": lambda: _stay(lambda: make_enrollment_codes_local(session)),
|
|
982
|
+
"mk codes": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
|
|
983
|
+
"mk code": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
|
|
984
|
+
"mk c": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
|
|
985
|
+
|
|
986
|
+
"codes list": lambda: _stay(lambda: list_enrollment_codes_local(session)),
|
|
987
|
+
"show c": lambda: _stay(lambda: list_enrollment_codes_local(session)), # old
|
|
988
|
+
|
|
989
|
+
"codes delete": lambda: _stay(lambda: remove_enrollment_code_local(session)),
|
|
990
|
+
"rm codes": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
|
|
991
|
+
"rm code": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
|
|
992
|
+
"rm c": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
|
|
993
|
+
|
|
994
|
+
# ---- host tunnel ----
|
|
995
|
+
"hosts status": lambda: _stay(_cmd_hosts_status),
|
|
996
|
+
"host status": lambda: _stay(_cmd_hosts_status),
|
|
997
|
+
|
|
998
|
+
"devices status": lambda: _stay(_cmd_devices_status),
|
|
999
|
+
"tunnel devices": lambda: _stay(_cmd_devices_status),
|
|
1000
|
+
|
|
1001
|
+
"codes csv": lambda: _stay(export_enrollment_codes_csv_local),
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
fn = DISPATCH.get(cmd)
|
|
1005
|
+
if fn is None:
|
|
1006
|
+
# allow: "hosts wipe" and "hosts wipe -y"
|
|
1007
|
+
if cmd.startswith("hosts wipe"):
|
|
1008
|
+
yes = ("-y" in cmd.split()) or ("--yes" in cmd.split())
|
|
1009
|
+
_cmd_hosts_wipe(yes=yes)
|
|
1010
|
+
return True
|
|
1011
|
+
|
|
1012
|
+
printf("Unknown command: ", Sty.RED, f"{inpu}", Sty.DEFAULT)
|
|
1013
|
+
printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands.", Sty.DEFAULT)
|
|
1014
|
+
return True
|
|
1015
|
+
|
|
1016
|
+
return fn()
|
|
1017
|
+
|
|
1018
|
+
# os.system('clear')
|
|
1019
|
+
welcome()
|
|
1020
|
+
|
|
1021
|
+
def main():
|
|
1022
|
+
while server_input(session.prompt(stylize('server@remote_rf: ', Sty.BOLD))):
|
|
1023
|
+
pass
|