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,1377 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import shutil
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Dict, List, Tuple
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import base64
|
|
12
|
+
import secrets
|
|
13
|
+
import hashlib
|
|
14
|
+
import getpass
|
|
15
|
+
|
|
16
|
+
from .host.host_auth_token import create_host_token, delete_host_token, wipe_all_host_tokens, show_host_tokens
|
|
17
|
+
|
|
18
|
+
def _x509_notafter_utc(cert_path: Path) -> Optional[datetime]:
|
|
19
|
+
# openssl output: notAfter=Feb 16 23:12:02 2027 GMT
|
|
20
|
+
rc, out = _run_capture(["openssl", "x509", "-in", str(cert_path), "-noout", "-enddate"])
|
|
21
|
+
if rc != 0 or not out:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
line = out.strip()
|
|
25
|
+
if not line.startswith("notAfter="):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
s = line.split("=", 1)[1].strip()
|
|
29
|
+
if s.endswith(" GMT"):
|
|
30
|
+
s = s[:-4].strip()
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
dt = datetime.strptime(s, "%b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc)
|
|
34
|
+
return dt
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fmt_remaining(na_utc: datetime) -> str:
|
|
40
|
+
now = datetime.now(timezone.utc)
|
|
41
|
+
delta = na_utc - now
|
|
42
|
+
secs = int(delta.total_seconds())
|
|
43
|
+
|
|
44
|
+
if secs <= 0:
|
|
45
|
+
return "EXPIRED"
|
|
46
|
+
|
|
47
|
+
days = secs // 86400
|
|
48
|
+
secs %= 86400
|
|
49
|
+
hours = secs // 3600
|
|
50
|
+
secs %= 3600
|
|
51
|
+
mins = secs // 60
|
|
52
|
+
|
|
53
|
+
return f"{days}d {hours}h {mins}m"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# -----------------------------
|
|
57
|
+
# Repo-local server config locations
|
|
58
|
+
# -----------------------------
|
|
59
|
+
|
|
60
|
+
def _repo_root() -> Path:
|
|
61
|
+
# <repo>/src/remoteRF_server/serverrf_cli.py -> parents[2] == <repo>
|
|
62
|
+
return Path(__file__).resolve().parents[2]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _xdg_config_home() -> Path:
|
|
66
|
+
return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _cfg_dir() -> Path:
|
|
70
|
+
# default: ~/.config/remoterf
|
|
71
|
+
return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _certs_dir() -> Path:
|
|
75
|
+
# allow override via REMOTERF_CERT_DIR (matches your server runtime)
|
|
76
|
+
return Path(os.getenv("REMOTERF_CERT_DIR", _cfg_dir() / "certs"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _server_env_path() -> Path:
|
|
80
|
+
return _cfg_dir() / "server.env"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _devices_env_path() -> Path:
|
|
84
|
+
# server-side device config storage (same schema as hostrf)
|
|
85
|
+
return _cfg_dir() / "devices.env"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _gen_certs_path() -> Path:
|
|
89
|
+
"""
|
|
90
|
+
Prefer repo-local tools next to this module, but allow repo-root /tools fallback.
|
|
91
|
+
- <repo>/src/remoteRF_server/tools/gen_certs.py
|
|
92
|
+
- <repo>/tools/gen_certs.py
|
|
93
|
+
"""
|
|
94
|
+
here = Path(__file__).resolve().parent
|
|
95
|
+
p1 = (here / "tools" / "gen_certs.py").resolve()
|
|
96
|
+
if p1.exists():
|
|
97
|
+
return p1
|
|
98
|
+
return (_repo_root() / "tools" / "gen_certs.py").resolve()
|
|
99
|
+
|
|
100
|
+
def _gist_env_path() -> Path:
|
|
101
|
+
# Stored alongside server.env/devices.env under ~/.config/remoterf by default
|
|
102
|
+
return _cfg_dir() / "gist.env"
|
|
103
|
+
|
|
104
|
+
# -----------------------------
|
|
105
|
+
# Help (exact commands only)
|
|
106
|
+
# -----------------------------
|
|
107
|
+
|
|
108
|
+
def print_help() -> None:
|
|
109
|
+
print(
|
|
110
|
+
"RemoteRF Server CLI Help\n"
|
|
111
|
+
"\n"
|
|
112
|
+
"Usage:\n"
|
|
113
|
+
" serverrf -h | --help\n"
|
|
114
|
+
"\n"
|
|
115
|
+
"Serve:\n"
|
|
116
|
+
" serverrf -s | --serve Run server (NOTE: only when -s/--serve is the FIRST arg)\n"
|
|
117
|
+
"\n"
|
|
118
|
+
"Certs:\n"
|
|
119
|
+
" serverrf --gen-certs <static_ip> [options]\n"
|
|
120
|
+
" serverrf --show-certs [-v]\n"
|
|
121
|
+
" serverrf --wipe-certs [-y]\n"
|
|
122
|
+
"\n"
|
|
123
|
+
"Ports:\n"
|
|
124
|
+
" serverrf -c | --config [options]\n"
|
|
125
|
+
"\n"
|
|
126
|
+
"Port options:\n"
|
|
127
|
+
" --main-port <int> Set GRPC_PORT\n"
|
|
128
|
+
" --cert-port <int> Set CERT_PORT\n"
|
|
129
|
+
" --show | -s Show current ports\n"
|
|
130
|
+
" -w | --wipe [-y] Wipe ONLY port config (server.env)\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"Hosts:\n"
|
|
133
|
+
" serverrf --host --token-create <host_id> [--length <int>] Create/rotate host token\n"
|
|
134
|
+
" serverrf --host --show Show all host token records\n"
|
|
135
|
+
" serverrf --host --delete <host_id> Delete host token record\n"
|
|
136
|
+
" serverrf --host --wipe [-y] Wipe ALL host token records\n"
|
|
137
|
+
" serverrf --host --list --secrets (optional) include salt/hash\n"
|
|
138
|
+
"\n"
|
|
139
|
+
"Devices:\n"
|
|
140
|
+
" serverrf -d | --device [options]\n"
|
|
141
|
+
"\n"
|
|
142
|
+
"Device options:\n"
|
|
143
|
+
" --add --pluto <id:name:iio_serial> Add device (fails if gid OR serial already used)\n"
|
|
144
|
+
" --remove <id> Remove device\n"
|
|
145
|
+
" --edit-name <id> <name> Override device NAME for existing device\n"
|
|
146
|
+
" --show | -s Show all devices\n"
|
|
147
|
+
" -w | --wipe [-y] Wipe ONLY device config (devices.env)\n"
|
|
148
|
+
|
|
149
|
+
"\n"
|
|
150
|
+
"Cert options (for --gen-certs):\n"
|
|
151
|
+
" --days <int> Server cert validity in days (default: 365)\n"
|
|
152
|
+
" --ca-days <int> CA cert validity in days (default: 3650)\n"
|
|
153
|
+
" --bits <int> RSA key size (default: 2048)\n"
|
|
154
|
+
" --dns <name> DNS SAN entry (repeatable)\n"
|
|
155
|
+
" --cn <name> Common Name (defaults to first SAN entry)\n"
|
|
156
|
+
" --force Overwrite existing certs/keys\n"
|
|
157
|
+
" --no-detect-ip Do not auto-detect IP when none provided\n"
|
|
158
|
+
"\n"
|
|
159
|
+
"Global:\n"
|
|
160
|
+
" -y, --yes Skip wipe confirmation prompts\n"
|
|
161
|
+
" -v, --verbose With --show-certs, also print x509 details (requires openssl)\n"
|
|
162
|
+
"\n"
|
|
163
|
+
"Gist Status:\n"
|
|
164
|
+
" serverrf --gist --set --id <gist_id> --file <filename> [--token <tok> | --token-stdin]\n"
|
|
165
|
+
" serverrf --gist --show [--secrets]\n"
|
|
166
|
+
" serverrf --gist --wipe [-y]\n"
|
|
167
|
+
"\n"
|
|
168
|
+
"Examples:\n"
|
|
169
|
+
" serverrf --gist --set --id 2a35e0... --file ucla-wlab-remoterf-status.json\n"
|
|
170
|
+
" serverrf --gist --set --id 2a35e0... --file status.json --token-stdin\n"
|
|
171
|
+
" serverrf --gist --show\n"
|
|
172
|
+
" serverrf --gist --wipe -y\n"
|
|
173
|
+
"\n"
|
|
174
|
+
# " serverrf --gen-certs 192.168.1.24 --dns rrf2 --dns rrf2.local --force\n"
|
|
175
|
+
" serverrf --gen-certs 192.168.1.50 --days 3650 --force\n"
|
|
176
|
+
" serverrf --show-certs -v\n"
|
|
177
|
+
" serverrf --wipe-certs -y\n"
|
|
178
|
+
"\n"
|
|
179
|
+
" serverrf --config --main-port 61005\n"
|
|
180
|
+
" serverrf --config --cert-port 61006\n"
|
|
181
|
+
" serverrf --config --show\n"
|
|
182
|
+
" serverrf -c -s\n"
|
|
183
|
+
" serverrf --config --wipe -y\n"
|
|
184
|
+
"\n"
|
|
185
|
+
" serverrf --host --token-create lab-host-01 --length 8 --force\n"
|
|
186
|
+
" serverrf --host --show\n"
|
|
187
|
+
" serverrf --host --delete lab-host-01\n"
|
|
188
|
+
" serverrf --host --wipe -y\n"
|
|
189
|
+
"\n"
|
|
190
|
+
" serverrf --device --add --pluto 0:pluto_aaa:123123\n"
|
|
191
|
+
" serverrf --device --edit-name 0 \"New Pluto Name\"\n"
|
|
192
|
+
" serverrf --device --remove 0\n"
|
|
193
|
+
" serverrf --device --show\n"
|
|
194
|
+
" serverrf --device --wipe -y\n"
|
|
195
|
+
"\n"
|
|
196
|
+
" serverrf -s\n"
|
|
197
|
+
" serverrf --serve\n"
|
|
198
|
+
"\n"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# -----------------------------
|
|
203
|
+
# Serve
|
|
204
|
+
# -----------------------------
|
|
205
|
+
|
|
206
|
+
def _serve() -> int:
|
|
207
|
+
"""
|
|
208
|
+
Alias for the RRRFserver entrypoint.
|
|
209
|
+
Equivalent to running the RRRFserver console_script.
|
|
210
|
+
"""
|
|
211
|
+
from remoteRF_server.server.grpc_server import main as grpc_main
|
|
212
|
+
grpc_main()
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# -----------------------------
|
|
217
|
+
# Tiny env utils (same style as hostrf)
|
|
218
|
+
# -----------------------------
|
|
219
|
+
|
|
220
|
+
def _write_env_kv(path: Path, kv: Dict[str, str]) -> None:
|
|
221
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
lines: List[str] = []
|
|
223
|
+
for k, v in kv.items():
|
|
224
|
+
v = str(v)
|
|
225
|
+
if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
|
|
226
|
+
v = v.replace('"', '\\"')
|
|
227
|
+
lines.append(f'{k}="{v}"')
|
|
228
|
+
else:
|
|
229
|
+
lines.append(f"{k}={v}")
|
|
230
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _read_env_kv(path: Path) -> Dict[str, str]:
|
|
234
|
+
out: Dict[str, str] = {}
|
|
235
|
+
if not path.exists():
|
|
236
|
+
return out
|
|
237
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
238
|
+
line = raw.strip()
|
|
239
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
240
|
+
continue
|
|
241
|
+
k, v = line.split("=", 1)
|
|
242
|
+
out[k.strip()] = v.strip().strip('"').strip("'")
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _validate_port(n: int, *, name: str) -> Optional[str]:
|
|
247
|
+
if n <= 0 or n > 65535:
|
|
248
|
+
return f"{name} out of range (1..65535)"
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def _redact(s: str) -> str:
|
|
252
|
+
s = (s or "").strip()
|
|
253
|
+
if len(s) <= 8:
|
|
254
|
+
return "****"
|
|
255
|
+
return f"{s[:4]}...{s[-4:]}"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _gist_show(*, show_secrets: bool = False) -> int:
|
|
259
|
+
p = _gist_env_path()
|
|
260
|
+
if not p.exists():
|
|
261
|
+
print(f"No gist config found (missing {p}).")
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
kv = _read_env_kv(p)
|
|
265
|
+
print(f"Gist config: {p}")
|
|
266
|
+
gid = kv.get("STATUS_GIST_ID", "")
|
|
267
|
+
fn = kv.get("STATUS_GIST_FILENAME", "")
|
|
268
|
+
tok = kv.get("GITHUB_TOKEN", "")
|
|
269
|
+
|
|
270
|
+
print(f" STATUS_GIST_ID={gid}")
|
|
271
|
+
print(f" STATUS_GIST_FILENAME={fn}")
|
|
272
|
+
if show_secrets:
|
|
273
|
+
print(f" GITHUB_TOKEN={tok}")
|
|
274
|
+
else:
|
|
275
|
+
print(f" GITHUB_TOKEN={_redact(tok)} (use --secrets to show)")
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _gist_set(*, gist_id: str, filename: str, token: str) -> int:
|
|
280
|
+
gist_id = (gist_id or "").strip()
|
|
281
|
+
filename = (filename or "").strip()
|
|
282
|
+
token = (token or "").strip()
|
|
283
|
+
|
|
284
|
+
if not gist_id:
|
|
285
|
+
print("ERROR: missing --id <gist_id>", file=sys.stderr)
|
|
286
|
+
return 2
|
|
287
|
+
if not filename:
|
|
288
|
+
print("ERROR: missing --file <filename>", file=sys.stderr)
|
|
289
|
+
return 2
|
|
290
|
+
if not token:
|
|
291
|
+
print("ERROR: missing token (use prompt or --token-stdin or --token)", file=sys.stderr)
|
|
292
|
+
return 2
|
|
293
|
+
|
|
294
|
+
_cfg_dir().mkdir(parents=True, exist_ok=True)
|
|
295
|
+
p = _gist_env_path()
|
|
296
|
+
|
|
297
|
+
kv = _read_env_kv(p)
|
|
298
|
+
kv["STATUS_GIST_ID"] = gist_id
|
|
299
|
+
kv["STATUS_GIST_FILENAME"] = filename
|
|
300
|
+
kv["GITHUB_TOKEN"] = token
|
|
301
|
+
|
|
302
|
+
_write_env_kv(p, kv)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
os.chmod(p, 0o600)
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
print("Configured gist status env:")
|
|
310
|
+
print(f" {p}")
|
|
311
|
+
print(f" STATUS_GIST_ID={gist_id}")
|
|
312
|
+
print(f" STATUS_GIST_FILENAME={filename}")
|
|
313
|
+
print(f" GITHUB_TOKEN={_redact(token)}")
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _gist_wipe(*, yes: bool) -> int:
|
|
318
|
+
p = _gist_env_path()
|
|
319
|
+
if not p.exists():
|
|
320
|
+
print(f"No gist config found at: {p}")
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
if not yes:
|
|
324
|
+
try:
|
|
325
|
+
if input("This will delete gist.env (gist status config). Type 'wipe' to confirm: ").strip().lower() != "wipe":
|
|
326
|
+
print("Wipe aborted.")
|
|
327
|
+
return 1
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
print("\nCancelled.")
|
|
330
|
+
return 1
|
|
331
|
+
|
|
332
|
+
p.unlink()
|
|
333
|
+
print("Wiped gist config (gist.env).")
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
# -----------------------------
|
|
337
|
+
# Subprocess utils
|
|
338
|
+
# -----------------------------
|
|
339
|
+
|
|
340
|
+
def _run(cmd: List[str]) -> int:
|
|
341
|
+
try:
|
|
342
|
+
subprocess.run(cmd, check=True)
|
|
343
|
+
return 0
|
|
344
|
+
except subprocess.CalledProcessError as e:
|
|
345
|
+
return int(e.returncode)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _run_capture(cmd: List[str]) -> Tuple[int, str]:
|
|
349
|
+
try:
|
|
350
|
+
p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
351
|
+
return 0, p.stdout
|
|
352
|
+
except subprocess.CalledProcessError as e:
|
|
353
|
+
out = ""
|
|
354
|
+
if getattr(e, "stdout", None):
|
|
355
|
+
out = e.stdout if isinstance(e.stdout, str) else e.stdout.decode(errors="replace")
|
|
356
|
+
return int(e.returncode), out
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# -----------------------------
|
|
360
|
+
# Cert actions
|
|
361
|
+
# -----------------------------
|
|
362
|
+
|
|
363
|
+
def _looks_like_pem_cert(p: Path) -> bool:
|
|
364
|
+
try:
|
|
365
|
+
data = p.read_bytes()
|
|
366
|
+
except Exception:
|
|
367
|
+
return False
|
|
368
|
+
return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _show_certs(*, verbose: bool) -> int:
|
|
372
|
+
d = _certs_dir()
|
|
373
|
+
print(f"Certs dir: {d}")
|
|
374
|
+
if not d.exists():
|
|
375
|
+
print(" (missing)")
|
|
376
|
+
return 1
|
|
377
|
+
|
|
378
|
+
files = sorted([p for p in d.iterdir() if p.is_file()])
|
|
379
|
+
if not files:
|
|
380
|
+
print(" (empty)")
|
|
381
|
+
return 1
|
|
382
|
+
|
|
383
|
+
for p in files:
|
|
384
|
+
try:
|
|
385
|
+
size = p.stat().st_size
|
|
386
|
+
except Exception:
|
|
387
|
+
size = -1
|
|
388
|
+
|
|
389
|
+
tag = ""
|
|
390
|
+
if p.suffix in (".crt", ".pem") and _looks_like_pem_cert(p):
|
|
391
|
+
tag = " [cert]"
|
|
392
|
+
elif p.suffix == ".key":
|
|
393
|
+
tag = " [key]"
|
|
394
|
+
print(f" - {p.name} ({size} bytes){tag}")
|
|
395
|
+
|
|
396
|
+
rc, _ = _run_capture(["openssl", "version"])
|
|
397
|
+
if rc != 0:
|
|
398
|
+
print("\n(openssl not available; install it to view cert expiry)")
|
|
399
|
+
else:
|
|
400
|
+
candidates: List[Path] = []
|
|
401
|
+
ca = d / "ca.crt"
|
|
402
|
+
srv = d / "server.crt"
|
|
403
|
+
if ca.exists():
|
|
404
|
+
candidates.append(ca)
|
|
405
|
+
if srv.exists():
|
|
406
|
+
candidates.append(srv)
|
|
407
|
+
|
|
408
|
+
for p in files:
|
|
409
|
+
if p in candidates:
|
|
410
|
+
continue
|
|
411
|
+
if p.suffix in (".crt", ".pem") and _looks_like_pem_cert(p):
|
|
412
|
+
candidates.append(p)
|
|
413
|
+
|
|
414
|
+
if candidates:
|
|
415
|
+
print("\nExpiry:")
|
|
416
|
+
for p in candidates:
|
|
417
|
+
na = _x509_notafter_utc(p)
|
|
418
|
+
if not na:
|
|
419
|
+
print(f" {p.name}: (could not read notAfter)")
|
|
420
|
+
continue
|
|
421
|
+
remaining = _fmt_remaining(na)
|
|
422
|
+
local = na.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
423
|
+
print(f" {p.name}: expires {local} ({remaining})")
|
|
424
|
+
else:
|
|
425
|
+
print("\nExpiry / Remaining:\n (no cert files found)")
|
|
426
|
+
|
|
427
|
+
if not verbose:
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
if rc != 0:
|
|
431
|
+
print("\n(openssl not available; install it to view cert details)")
|
|
432
|
+
return 0
|
|
433
|
+
|
|
434
|
+
print("\nCertificate details (openssl):")
|
|
435
|
+
for p in files:
|
|
436
|
+
if p.suffix not in (".crt", ".pem"):
|
|
437
|
+
continue
|
|
438
|
+
if not _looks_like_pem_cert(p):
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
print(f"\n== {p.name} ==")
|
|
442
|
+
rc2, out2 = _run_capture(["openssl", "x509", "-in", str(p), "-noout", "-subject", "-issuer", "-dates"])
|
|
443
|
+
if rc2 == 0 and out2.strip():
|
|
444
|
+
print(out2.rstrip())
|
|
445
|
+
else:
|
|
446
|
+
print(" (failed to parse with openssl)")
|
|
447
|
+
|
|
448
|
+
rc2, out2 = _run_capture(["openssl", "x509", "-in", str(p), "-noout", "-ext", "subjectAltName"])
|
|
449
|
+
if rc2 == 0 and out2.strip():
|
|
450
|
+
print(out2.rstrip())
|
|
451
|
+
|
|
452
|
+
return 0
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _wipe_certs(*, yes: bool) -> int:
|
|
456
|
+
d = _certs_dir()
|
|
457
|
+
if not d.exists():
|
|
458
|
+
print(f"No cert output found at: {d}")
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
if not yes:
|
|
462
|
+
try:
|
|
463
|
+
if input(f"This will delete ALL cert outputs at:\n {d}\nType 'wipe' to confirm: ").strip().lower() != "wipe":
|
|
464
|
+
print("Wipe aborted.")
|
|
465
|
+
return 1
|
|
466
|
+
except KeyboardInterrupt:
|
|
467
|
+
print("\nCancelled.")
|
|
468
|
+
return 1
|
|
469
|
+
|
|
470
|
+
shutil.rmtree(d)
|
|
471
|
+
print(f"Wiped cert output: {d}")
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _gen_certs(
|
|
476
|
+
*,
|
|
477
|
+
static_ip: str,
|
|
478
|
+
days: int,
|
|
479
|
+
ca_days: int,
|
|
480
|
+
bits: int,
|
|
481
|
+
dns: List[str],
|
|
482
|
+
cn: Optional[str],
|
|
483
|
+
force: bool,
|
|
484
|
+
no_detect_ip: bool,
|
|
485
|
+
) -> int:
|
|
486
|
+
gen_path = _gen_certs_path()
|
|
487
|
+
if not gen_path.exists():
|
|
488
|
+
print(f"ERROR: cert generator not found: {gen_path}", file=sys.stderr)
|
|
489
|
+
return 2
|
|
490
|
+
|
|
491
|
+
_cfg_dir().mkdir(parents=True, exist_ok=True)
|
|
492
|
+
|
|
493
|
+
cmd: List[str] = [
|
|
494
|
+
sys.executable,
|
|
495
|
+
str(gen_path),
|
|
496
|
+
"--days", str(days),
|
|
497
|
+
"--ca-days", str(ca_days),
|
|
498
|
+
"--bits", str(bits),
|
|
499
|
+
"--config-dir", str(_cfg_dir()),
|
|
500
|
+
"--out-subdir", "certs",
|
|
501
|
+
"--ip", static_ip,
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
if force:
|
|
505
|
+
cmd.append("--force")
|
|
506
|
+
if no_detect_ip:
|
|
507
|
+
cmd.append("--no-detect-ip")
|
|
508
|
+
for d in dns:
|
|
509
|
+
cmd += ["--dns", d]
|
|
510
|
+
if cn:
|
|
511
|
+
cmd += ["--cn", cn]
|
|
512
|
+
|
|
513
|
+
rc = _run(cmd)
|
|
514
|
+
if rc == 0:
|
|
515
|
+
print("Generated certs:")
|
|
516
|
+
print(f" {_certs_dir()}")
|
|
517
|
+
|
|
518
|
+
print("\nUsers/Hosts must reconfigure each time new certs are generated.")
|
|
519
|
+
return rc
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# -----------------------------
|
|
523
|
+
# Port config actions
|
|
524
|
+
# -----------------------------
|
|
525
|
+
|
|
526
|
+
def _config_show_ports() -> int:
|
|
527
|
+
p = _server_env_path()
|
|
528
|
+
if not p.exists():
|
|
529
|
+
print(f"No port config found (missing {p}).")
|
|
530
|
+
return 0
|
|
531
|
+
|
|
532
|
+
kv = _read_env_kv(p)
|
|
533
|
+
if not kv:
|
|
534
|
+
print("Port config is empty.")
|
|
535
|
+
return 0
|
|
536
|
+
|
|
537
|
+
print(f"Port config: {p}")
|
|
538
|
+
if "GRPC_PORT" in kv:
|
|
539
|
+
print(f" GRPC_PORT={kv['GRPC_PORT']}")
|
|
540
|
+
if "CERT_PORT" in kv:
|
|
541
|
+
print(f" CERT_PORT={kv['CERT_PORT']}")
|
|
542
|
+
return 0
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _config_set_ports(*, main_port: Optional[int], cert_port: Optional[int]) -> int:
|
|
546
|
+
if main_port is None and cert_port is None:
|
|
547
|
+
print("ERROR: expected --main-port <int> and/or --cert-port <int>", file=sys.stderr)
|
|
548
|
+
return 2
|
|
549
|
+
|
|
550
|
+
if main_port is not None:
|
|
551
|
+
err = _validate_port(main_port, name="main-port")
|
|
552
|
+
if err:
|
|
553
|
+
print(f"ERROR: {err}", file=sys.stderr)
|
|
554
|
+
return 2
|
|
555
|
+
|
|
556
|
+
if cert_port is not None:
|
|
557
|
+
err = _validate_port(cert_port, name="cert-port")
|
|
558
|
+
if err:
|
|
559
|
+
print(f"ERROR: {err}", file=sys.stderr)
|
|
560
|
+
return 2
|
|
561
|
+
|
|
562
|
+
_cfg_dir().mkdir(parents=True, exist_ok=True)
|
|
563
|
+
p = _server_env_path()
|
|
564
|
+
kv = _read_env_kv(p)
|
|
565
|
+
|
|
566
|
+
if main_port is not None:
|
|
567
|
+
kv["GRPC_PORT"] = str(main_port)
|
|
568
|
+
if cert_port is not None:
|
|
569
|
+
kv["CERT_PORT"] = str(cert_port)
|
|
570
|
+
|
|
571
|
+
_write_env_kv(p, kv)
|
|
572
|
+
print("Configured ports:")
|
|
573
|
+
print(f" {p}")
|
|
574
|
+
if main_port is not None:
|
|
575
|
+
print(f" GRPC_PORT={main_port}")
|
|
576
|
+
if cert_port is not None:
|
|
577
|
+
print(f" CERT_PORT={cert_port}")
|
|
578
|
+
return 0
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _config_wipe_ports(*, yes: bool) -> int:
|
|
582
|
+
p = _server_env_path()
|
|
583
|
+
if not p.exists():
|
|
584
|
+
print(f"No port config found at: {p}")
|
|
585
|
+
return 0
|
|
586
|
+
|
|
587
|
+
if not yes:
|
|
588
|
+
try:
|
|
589
|
+
if input("This will delete server.env (ports). Type 'wipe' to confirm: ").strip().lower() != "wipe":
|
|
590
|
+
print("Wipe aborted.")
|
|
591
|
+
return 1
|
|
592
|
+
except KeyboardInterrupt:
|
|
593
|
+
print("\nCancelled.")
|
|
594
|
+
return 1
|
|
595
|
+
|
|
596
|
+
p.unlink()
|
|
597
|
+
print("Wiped port config (server.env).")
|
|
598
|
+
return 0
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# -----------------------------
|
|
602
|
+
# Device storage/actions
|
|
603
|
+
# -----------------------------
|
|
604
|
+
|
|
605
|
+
_DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)
|
|
606
|
+
|
|
607
|
+
def _parse_pluto_add_spec(tok: str) -> Tuple[int, str, str]:
|
|
608
|
+
# <gid:name:iio_serial>
|
|
609
|
+
parts = [p.strip() for p in (tok or "").split(":")]
|
|
610
|
+
if len(parts) != 3:
|
|
611
|
+
raise ValueError("Expected <gid:name:iio_serial>")
|
|
612
|
+
gid_s, name, iio_serial = parts
|
|
613
|
+
gid = int(gid_s)
|
|
614
|
+
if gid < 0:
|
|
615
|
+
raise ValueError("gid must be >= 0")
|
|
616
|
+
if not name:
|
|
617
|
+
raise ValueError("name is empty")
|
|
618
|
+
if not iio_serial:
|
|
619
|
+
raise ValueError("iio_serial is empty")
|
|
620
|
+
return gid, name, iio_serial
|
|
621
|
+
|
|
622
|
+
def _device_edit_name(global_id_str: str, new_name: str) -> int:
|
|
623
|
+
try:
|
|
624
|
+
gid = int((global_id_str or "").strip())
|
|
625
|
+
except Exception:
|
|
626
|
+
print("ERROR: --edit-name expects <gid:int> <new_name>", file=sys.stderr)
|
|
627
|
+
return 2
|
|
628
|
+
|
|
629
|
+
name = (new_name or "").strip()
|
|
630
|
+
if not name:
|
|
631
|
+
print("ERROR: new_name is empty", file=sys.stderr)
|
|
632
|
+
return 2
|
|
633
|
+
|
|
634
|
+
recs = _read_device_records()
|
|
635
|
+
if gid not in recs:
|
|
636
|
+
print(f"ERROR: global_id {gid} not present; cannot edit name.", file=sys.stderr)
|
|
637
|
+
return 2
|
|
638
|
+
|
|
639
|
+
recs[gid]["NAME"] = name
|
|
640
|
+
_write_device_records(recs)
|
|
641
|
+
print(f"Updated device name: {gid} name={name}")
|
|
642
|
+
return 0
|
|
643
|
+
|
|
644
|
+
def _read_device_records() -> Dict[int, Dict[str, str]]:
|
|
645
|
+
kv = _read_env_kv(_devices_env_path())
|
|
646
|
+
recs: Dict[int, Dict[str, str]] = {}
|
|
647
|
+
for k, v in kv.items():
|
|
648
|
+
m = _DEVICE_KEY_RE.match(k.strip())
|
|
649
|
+
if not m:
|
|
650
|
+
continue
|
|
651
|
+
gid = int(m.group(1))
|
|
652
|
+
field = m.group(2).upper().strip()
|
|
653
|
+
recs.setdefault(gid, {})[field] = str(v).strip()
|
|
654
|
+
return recs
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _write_device_records(recs: Dict[int, Dict[str, str]]) -> None:
|
|
658
|
+
out: Dict[str, str] = {}
|
|
659
|
+
for gid in sorted(recs.keys()):
|
|
660
|
+
fields = recs[gid]
|
|
661
|
+
order = ["TYPE", "NAME", "IDENT_KIND", "IDENT"]
|
|
662
|
+
for f in order:
|
|
663
|
+
if f in fields and fields[f] != "":
|
|
664
|
+
out[f"DEVICE_{gid}_{f}"] = fields[f]
|
|
665
|
+
for f in sorted(fields.keys()):
|
|
666
|
+
if f in order:
|
|
667
|
+
continue
|
|
668
|
+
if fields[f] != "":
|
|
669
|
+
out[f"DEVICE_{gid}_{f}"] = fields[f]
|
|
670
|
+
_write_env_kv(_devices_env_path(), out)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _device_add_pluto(spec: str) -> int:
|
|
674
|
+
try:
|
|
675
|
+
gid, name, iio_serial = _parse_pluto_add_spec(spec)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
print(f"ERROR: invalid pluto spec: {e}", file=sys.stderr)
|
|
678
|
+
return 2
|
|
679
|
+
|
|
680
|
+
_cfg_dir().mkdir(parents=True, exist_ok=True)
|
|
681
|
+
recs = _read_device_records()
|
|
682
|
+
|
|
683
|
+
if gid in recs:
|
|
684
|
+
print(f"ERROR: global_id {gid} already exists", file=sys.stderr)
|
|
685
|
+
return 2
|
|
686
|
+
|
|
687
|
+
for other_gid, r in recs.items():
|
|
688
|
+
if r.get("IDENT_KIND", "") == "iio_serial" and r.get("IDENT", "") == iio_serial:
|
|
689
|
+
print(f"ERROR: iio_serial already used by global_id={other_gid}", file=sys.stderr)
|
|
690
|
+
return 2
|
|
691
|
+
|
|
692
|
+
recs[gid] = {
|
|
693
|
+
"TYPE": "pluto",
|
|
694
|
+
"NAME": name,
|
|
695
|
+
"IDENT_KIND": "iio_serial",
|
|
696
|
+
"IDENT": iio_serial,
|
|
697
|
+
}
|
|
698
|
+
_write_device_records(recs)
|
|
699
|
+
|
|
700
|
+
print(f"Added device: {gid} type=pluto name={name} iio_serial={iio_serial}")
|
|
701
|
+
return 0
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _device_remove(global_id_str: str) -> int:
|
|
705
|
+
try:
|
|
706
|
+
gid = int((global_id_str or "").strip())
|
|
707
|
+
except Exception:
|
|
708
|
+
print("ERROR: --remove expects <gid:int>", file=sys.stderr)
|
|
709
|
+
return 2
|
|
710
|
+
|
|
711
|
+
recs = _read_device_records()
|
|
712
|
+
if gid not in recs:
|
|
713
|
+
print(f"WARNING: global_id {gid} not present; nothing to remove.")
|
|
714
|
+
return 0
|
|
715
|
+
|
|
716
|
+
recs.pop(gid, None)
|
|
717
|
+
_write_device_records(recs)
|
|
718
|
+
print(f"Removed device: {gid}")
|
|
719
|
+
return 0
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _device_show() -> int:
|
|
723
|
+
p = _devices_env_path()
|
|
724
|
+
if not p.exists():
|
|
725
|
+
print(f"No devices configured (missing {p}).")
|
|
726
|
+
return 0
|
|
727
|
+
|
|
728
|
+
recs = _read_device_records()
|
|
729
|
+
print(f"Devices: {p}")
|
|
730
|
+
if not recs:
|
|
731
|
+
print(" (none)")
|
|
732
|
+
return 0
|
|
733
|
+
|
|
734
|
+
for gid in sorted(recs.keys()):
|
|
735
|
+
r = recs[gid]
|
|
736
|
+
dtype = r.get("TYPE", "")
|
|
737
|
+
name = r.get("NAME", "")
|
|
738
|
+
ik = r.get("IDENT_KIND", "")
|
|
739
|
+
ident = r.get("IDENT", "")
|
|
740
|
+
print(f" {gid}: type={dtype} name={name} {ik}={ident}")
|
|
741
|
+
return 0
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _wipe_devices_only(*, yes: bool) -> int:
|
|
745
|
+
p = _devices_env_path()
|
|
746
|
+
if not p.exists():
|
|
747
|
+
print(f"No device config found at: {p}")
|
|
748
|
+
return 0
|
|
749
|
+
|
|
750
|
+
if not yes:
|
|
751
|
+
try:
|
|
752
|
+
if input("This will delete devices.env. Type 'wipe' to confirm: ").strip().lower() != "wipe":
|
|
753
|
+
print("Wipe aborted.")
|
|
754
|
+
return 1
|
|
755
|
+
except KeyboardInterrupt:
|
|
756
|
+
print("\nCancelled.")
|
|
757
|
+
return 1
|
|
758
|
+
|
|
759
|
+
p.unlink()
|
|
760
|
+
print("Wiped device config (devices.env).")
|
|
761
|
+
return 0
|
|
762
|
+
|
|
763
|
+
# -----------------------------
|
|
764
|
+
# CLI parser (ONLY commands in help)
|
|
765
|
+
# -----------------------------
|
|
766
|
+
def main() -> int:
|
|
767
|
+
argv = list(sys.argv[1:])
|
|
768
|
+
|
|
769
|
+
# help
|
|
770
|
+
if len(argv) == 0 or argv[0] in ("--help", "-h", "-help", "--h"):
|
|
771
|
+
print_help()
|
|
772
|
+
return 0
|
|
773
|
+
|
|
774
|
+
# IMPORTANT: ONLY "serverrf -s/--serve" (first arg) starts the server.
|
|
775
|
+
if argv[0] in ("--serve", "-s"):
|
|
776
|
+
if len(argv) != 1:
|
|
777
|
+
print("ERROR: -s/--serve cannot be combined with other commands. Run it alone.", file=sys.stderr)
|
|
778
|
+
return 2
|
|
779
|
+
return _serve()
|
|
780
|
+
|
|
781
|
+
yes = False
|
|
782
|
+
verbose = False
|
|
783
|
+
|
|
784
|
+
# cert commands
|
|
785
|
+
gen_ip: Optional[str] = None
|
|
786
|
+
show_certs = False
|
|
787
|
+
wipe_certs = False
|
|
788
|
+
|
|
789
|
+
# gen-certs options
|
|
790
|
+
days = 365
|
|
791
|
+
ca_days = 3650
|
|
792
|
+
bits = 2048
|
|
793
|
+
dns: List[str] = []
|
|
794
|
+
cn: Optional[str] = None
|
|
795
|
+
force = False
|
|
796
|
+
no_detect_ip = False
|
|
797
|
+
|
|
798
|
+
# config commands
|
|
799
|
+
config_mode = False
|
|
800
|
+
config_show = False
|
|
801
|
+
config_wipe = False
|
|
802
|
+
main_port: Optional[int] = None
|
|
803
|
+
cert_port: Optional[int] = None
|
|
804
|
+
|
|
805
|
+
# device commands
|
|
806
|
+
device_mode = False
|
|
807
|
+
device_show = False
|
|
808
|
+
device_wipe = False
|
|
809
|
+
device_add_type: Optional[str] = None
|
|
810
|
+
device_add_spec: Optional[str] = None
|
|
811
|
+
device_remove_gid: Optional[str] = None
|
|
812
|
+
device_edit_gid: Optional[str] = None
|
|
813
|
+
device_edit_new_name: Optional[str] = None
|
|
814
|
+
|
|
815
|
+
# host commands
|
|
816
|
+
host_mode = False
|
|
817
|
+
host_token_create_id: Optional[str] = None
|
|
818
|
+
host_delete_id: Optional[str] = None
|
|
819
|
+
host_list = False
|
|
820
|
+
host_wipe = False
|
|
821
|
+
host_show_secrets = False # optional
|
|
822
|
+
host_token_length: Optional[int] = None
|
|
823
|
+
host_force = False # host-only overwrite/rotate
|
|
824
|
+
|
|
825
|
+
# gist commands
|
|
826
|
+
gist_mode = False
|
|
827
|
+
gist_show = False
|
|
828
|
+
gist_wipe = False
|
|
829
|
+
gist_set = False
|
|
830
|
+
gist_show_secrets = False
|
|
831
|
+
|
|
832
|
+
gist_id: Optional[str] = None
|
|
833
|
+
gist_filename: Optional[str] = None
|
|
834
|
+
gist_token: Optional[str] = None
|
|
835
|
+
gist_token_stdin = False
|
|
836
|
+
|
|
837
|
+
i = 0
|
|
838
|
+
while i < len(argv):
|
|
839
|
+
tok = argv[i]
|
|
840
|
+
|
|
841
|
+
if tok in ("-y", "--yes", "-yes"):
|
|
842
|
+
yes = True
|
|
843
|
+
i += 1
|
|
844
|
+
continue
|
|
845
|
+
|
|
846
|
+
if tok in ("-v", "--verbose", "-verbose"):
|
|
847
|
+
verbose = True
|
|
848
|
+
i += 1
|
|
849
|
+
continue
|
|
850
|
+
|
|
851
|
+
# enter config mode
|
|
852
|
+
if tok in ("--config", "-c", "-config"):
|
|
853
|
+
config_mode = True
|
|
854
|
+
i += 1
|
|
855
|
+
continue
|
|
856
|
+
|
|
857
|
+
# enter device mode
|
|
858
|
+
if tok in ("--device", "-d", "-device"):
|
|
859
|
+
device_mode = True
|
|
860
|
+
i += 1
|
|
861
|
+
continue
|
|
862
|
+
|
|
863
|
+
# cert verbs
|
|
864
|
+
if tok == "--show-certs":
|
|
865
|
+
show_certs = True
|
|
866
|
+
i += 1
|
|
867
|
+
continue
|
|
868
|
+
|
|
869
|
+
if tok == "--wipe-certs":
|
|
870
|
+
wipe_certs = True
|
|
871
|
+
i += 1
|
|
872
|
+
continue
|
|
873
|
+
|
|
874
|
+
if tok == "--gen-certs":
|
|
875
|
+
if i + 1 >= len(argv):
|
|
876
|
+
print("ERROR: missing value after --gen-certs <static_ip>", file=sys.stderr)
|
|
877
|
+
return 2
|
|
878
|
+
gen_ip = argv[i + 1].strip()
|
|
879
|
+
i += 2
|
|
880
|
+
continue
|
|
881
|
+
|
|
882
|
+
# config verbs/options
|
|
883
|
+
if config_mode and tok in ("--show", "-show", "-s"):
|
|
884
|
+
config_show = True
|
|
885
|
+
i += 1
|
|
886
|
+
continue
|
|
887
|
+
|
|
888
|
+
if config_mode and tok in ("-w", "--wipe", "-wipe"):
|
|
889
|
+
config_wipe = True
|
|
890
|
+
i += 1
|
|
891
|
+
continue
|
|
892
|
+
|
|
893
|
+
if config_mode and tok == "--main-port":
|
|
894
|
+
if i + 1 >= len(argv):
|
|
895
|
+
print("ERROR: missing value after --main-port <int>", file=sys.stderr)
|
|
896
|
+
return 2
|
|
897
|
+
try:
|
|
898
|
+
main_port = int(argv[i + 1].strip())
|
|
899
|
+
except Exception:
|
|
900
|
+
print("ERROR: --main-port expects an int", file=sys.stderr)
|
|
901
|
+
return 2
|
|
902
|
+
i += 2
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
if config_mode and tok == "--cert-port":
|
|
906
|
+
if i + 1 >= len(argv):
|
|
907
|
+
print("ERROR: missing value after --cert-port <int>", file=sys.stderr)
|
|
908
|
+
return 2
|
|
909
|
+
try:
|
|
910
|
+
cert_port = int(argv[i + 1].strip())
|
|
911
|
+
except Exception:
|
|
912
|
+
print("ERROR: --cert-port expects an int", file=sys.stderr)
|
|
913
|
+
return 2
|
|
914
|
+
i += 2
|
|
915
|
+
continue
|
|
916
|
+
|
|
917
|
+
# enter host mode
|
|
918
|
+
if tok in ("--host", "-host"):
|
|
919
|
+
host_mode = True
|
|
920
|
+
i += 1
|
|
921
|
+
continue
|
|
922
|
+
|
|
923
|
+
# host verbs
|
|
924
|
+
if host_mode and tok in ("--token-create", "-token-create"):
|
|
925
|
+
if i + 1 >= len(argv):
|
|
926
|
+
print("ERROR: --token-create expects <host_id>", file=sys.stderr)
|
|
927
|
+
return 2
|
|
928
|
+
host_token_create_id = argv[i + 1].strip()
|
|
929
|
+
i += 2
|
|
930
|
+
continue
|
|
931
|
+
|
|
932
|
+
if host_mode and tok in ("--length", "-length", "--token-length"):
|
|
933
|
+
if i + 1 >= len(argv):
|
|
934
|
+
print("ERROR: --length expects <int>", file=sys.stderr)
|
|
935
|
+
return 2
|
|
936
|
+
try:
|
|
937
|
+
host_token_length = int(argv[i + 1].strip())
|
|
938
|
+
except Exception:
|
|
939
|
+
print("ERROR: --length expects an int", file=sys.stderr)
|
|
940
|
+
return 2
|
|
941
|
+
if host_token_length < 4 or host_token_length > 64:
|
|
942
|
+
print("ERROR: --length out of range (4..64)", file=sys.stderr)
|
|
943
|
+
return 2
|
|
944
|
+
i += 2
|
|
945
|
+
continue
|
|
946
|
+
|
|
947
|
+
if host_mode and tok in ("--delete", "-delete", "--token-delete", "-token-delete"):
|
|
948
|
+
if i + 1 >= len(argv):
|
|
949
|
+
print("ERROR: --delete expects <host_id>", file=sys.stderr)
|
|
950
|
+
return 2
|
|
951
|
+
host_delete_id = argv[i + 1].strip()
|
|
952
|
+
i += 2
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
if host_mode and tok in ("--list", "-list", "--show", "-show"):
|
|
956
|
+
host_list = True
|
|
957
|
+
i += 1
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
if host_mode and tok in ("--wipe", "-wipe", "-w"):
|
|
961
|
+
host_wipe = True
|
|
962
|
+
i += 1
|
|
963
|
+
continue
|
|
964
|
+
|
|
965
|
+
if host_mode and tok in ("--secrets", "-secrets"):
|
|
966
|
+
host_show_secrets = True
|
|
967
|
+
i += 1
|
|
968
|
+
continue
|
|
969
|
+
|
|
970
|
+
if host_mode and tok in ("--force", "-f", "--rotate"):
|
|
971
|
+
host_force = True
|
|
972
|
+
i += 1
|
|
973
|
+
continue
|
|
974
|
+
|
|
975
|
+
# enter gist mode
|
|
976
|
+
if tok in ("--gist", "-gist"):
|
|
977
|
+
gist_mode = True
|
|
978
|
+
i += 1
|
|
979
|
+
continue
|
|
980
|
+
|
|
981
|
+
# gist verbs/options
|
|
982
|
+
if gist_mode and tok in ("--show", "-show", "-s"):
|
|
983
|
+
gist_show = True
|
|
984
|
+
i += 1
|
|
985
|
+
continue
|
|
986
|
+
|
|
987
|
+
if gist_mode and tok in ("--wipe", "-wipe", "-w"):
|
|
988
|
+
gist_wipe = True
|
|
989
|
+
i += 1
|
|
990
|
+
continue
|
|
991
|
+
|
|
992
|
+
if gist_mode and tok in ("--set", "-set", "--config", "--configure"):
|
|
993
|
+
gist_set = True
|
|
994
|
+
i += 1
|
|
995
|
+
continue
|
|
996
|
+
|
|
997
|
+
if gist_mode and tok in ("--secrets", "-secrets"):
|
|
998
|
+
gist_show_secrets = True
|
|
999
|
+
i += 1
|
|
1000
|
+
continue
|
|
1001
|
+
|
|
1002
|
+
if gist_mode and tok == "--id":
|
|
1003
|
+
if i + 1 >= len(argv):
|
|
1004
|
+
print("ERROR: --id expects <gist_id>", file=sys.stderr)
|
|
1005
|
+
return 2
|
|
1006
|
+
gist_id = argv[i + 1].strip()
|
|
1007
|
+
i += 2
|
|
1008
|
+
continue
|
|
1009
|
+
|
|
1010
|
+
if gist_mode and tok in ("--file", "--filename"):
|
|
1011
|
+
if i + 1 >= len(argv):
|
|
1012
|
+
print("ERROR: --file expects <filename>", file=sys.stderr)
|
|
1013
|
+
return 2
|
|
1014
|
+
gist_filename = argv[i + 1].strip()
|
|
1015
|
+
i += 2
|
|
1016
|
+
continue
|
|
1017
|
+
|
|
1018
|
+
# Not recommended (shell history), but provided anyway
|
|
1019
|
+
if gist_mode and tok == "--token":
|
|
1020
|
+
if i + 1 >= len(argv):
|
|
1021
|
+
print("ERROR: --token expects <token>", file=sys.stderr)
|
|
1022
|
+
return 2
|
|
1023
|
+
gist_token = argv[i + 1].strip()
|
|
1024
|
+
i += 2
|
|
1025
|
+
continue
|
|
1026
|
+
|
|
1027
|
+
if gist_mode and tok == "--token-stdin":
|
|
1028
|
+
gist_token_stdin = True
|
|
1029
|
+
i += 1
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1032
|
+
# device verbs/options
|
|
1033
|
+
if device_mode and tok in ("--show", "-show", "-s"):
|
|
1034
|
+
device_show = True
|
|
1035
|
+
i += 1
|
|
1036
|
+
continue
|
|
1037
|
+
|
|
1038
|
+
if device_mode and tok in ("-w", "--wipe", "-wipe"):
|
|
1039
|
+
device_wipe = True
|
|
1040
|
+
i += 1
|
|
1041
|
+
continue
|
|
1042
|
+
|
|
1043
|
+
if device_mode and tok in ("--add", "-add", "-a"):
|
|
1044
|
+
if i + 1 >= len(argv):
|
|
1045
|
+
print("ERROR: missing device type after --device --add (expected --pluto)", file=sys.stderr)
|
|
1046
|
+
return 2
|
|
1047
|
+
dt = argv[i + 1].strip()
|
|
1048
|
+
if dt != "--pluto":
|
|
1049
|
+
print("ERROR: only '--device --add --pluto <gid:name:iio_serial>' is supported", file=sys.stderr)
|
|
1050
|
+
return 2
|
|
1051
|
+
if i + 2 >= len(argv):
|
|
1052
|
+
print("ERROR: missing spec after --device --add --pluto", file=sys.stderr)
|
|
1053
|
+
return 2
|
|
1054
|
+
device_add_type = "pluto"
|
|
1055
|
+
device_add_spec = argv[i + 2].strip()
|
|
1056
|
+
i += 3
|
|
1057
|
+
continue
|
|
1058
|
+
|
|
1059
|
+
if device_mode and tok in ("--remove", "-remove", "-r"):
|
|
1060
|
+
if i + 1 >= len(argv):
|
|
1061
|
+
print("ERROR: missing value after --device --remove <gid>", file=sys.stderr)
|
|
1062
|
+
return 2
|
|
1063
|
+
device_remove_gid = argv[i + 1].strip()
|
|
1064
|
+
i += 2
|
|
1065
|
+
continue
|
|
1066
|
+
|
|
1067
|
+
if device_mode and tok in ("--edit-name", "-edit-name"):
|
|
1068
|
+
if i + 2 >= len(argv):
|
|
1069
|
+
print("ERROR: --edit-name expects <gid> <new_name>", file=sys.stderr)
|
|
1070
|
+
return 2
|
|
1071
|
+
device_edit_gid = argv[i + 1].strip()
|
|
1072
|
+
device_edit_new_name = argv[i + 2].strip()
|
|
1073
|
+
i += 3
|
|
1074
|
+
continue
|
|
1075
|
+
|
|
1076
|
+
# gen-certs options
|
|
1077
|
+
if tok == "--days":
|
|
1078
|
+
if i + 1 >= len(argv):
|
|
1079
|
+
print("ERROR: missing value after --days <int>", file=sys.stderr)
|
|
1080
|
+
return 2
|
|
1081
|
+
try:
|
|
1082
|
+
days = int(argv[i + 1].strip())
|
|
1083
|
+
except Exception:
|
|
1084
|
+
print("ERROR: --days expects an int", file=sys.stderr)
|
|
1085
|
+
return 2
|
|
1086
|
+
i += 2
|
|
1087
|
+
continue
|
|
1088
|
+
|
|
1089
|
+
if tok == "--ca-days":
|
|
1090
|
+
if i + 1 >= len(argv):
|
|
1091
|
+
print("ERROR: missing value after --ca-days <int>", file=sys.stderr)
|
|
1092
|
+
return 2
|
|
1093
|
+
try:
|
|
1094
|
+
ca_days = int(argv[i + 1].strip())
|
|
1095
|
+
except Exception:
|
|
1096
|
+
print("ERROR: --ca-days expects an int", file=sys.stderr)
|
|
1097
|
+
return 2
|
|
1098
|
+
i += 2
|
|
1099
|
+
continue
|
|
1100
|
+
|
|
1101
|
+
if tok == "--bits":
|
|
1102
|
+
if i + 1 >= len(argv):
|
|
1103
|
+
print("ERROR: missing value after --bits <int>", file=sys.stderr)
|
|
1104
|
+
return 2
|
|
1105
|
+
try:
|
|
1106
|
+
bits = int(argv[i + 1].strip())
|
|
1107
|
+
except Exception:
|
|
1108
|
+
print("ERROR: --bits expects an int", file=sys.stderr)
|
|
1109
|
+
return 2
|
|
1110
|
+
i += 2
|
|
1111
|
+
continue
|
|
1112
|
+
|
|
1113
|
+
if tok == "--dns":
|
|
1114
|
+
if i + 1 >= len(argv):
|
|
1115
|
+
print("ERROR: missing value after --dns <name>", file=sys.stderr)
|
|
1116
|
+
return 2
|
|
1117
|
+
dns.append(argv[i + 1].strip())
|
|
1118
|
+
i += 2
|
|
1119
|
+
continue
|
|
1120
|
+
|
|
1121
|
+
if tok == "--cn":
|
|
1122
|
+
if i + 1 >= len(argv):
|
|
1123
|
+
print("ERROR: missing value after --cn <name>", file=sys.stderr)
|
|
1124
|
+
return 2
|
|
1125
|
+
cn = argv[i + 1].strip()
|
|
1126
|
+
i += 2
|
|
1127
|
+
continue
|
|
1128
|
+
|
|
1129
|
+
if tok == "--force":
|
|
1130
|
+
force = True
|
|
1131
|
+
i += 1
|
|
1132
|
+
continue
|
|
1133
|
+
|
|
1134
|
+
if tok == "--no-detect-ip":
|
|
1135
|
+
no_detect_ip = True
|
|
1136
|
+
i += 1
|
|
1137
|
+
continue
|
|
1138
|
+
|
|
1139
|
+
print(f"ERROR: unknown option: {tok!r}", file=sys.stderr)
|
|
1140
|
+
return 2
|
|
1141
|
+
|
|
1142
|
+
# ----------------
|
|
1143
|
+
# Enforce single-mode semantics (device vs config vs cert)
|
|
1144
|
+
# ----------------
|
|
1145
|
+
if device_mode and (config_mode or show_certs or wipe_certs or (gen_ip is not None)):
|
|
1146
|
+
print("ERROR: --device cannot be combined with config/cert commands. Run them separately.", file=sys.stderr)
|
|
1147
|
+
return 2
|
|
1148
|
+
|
|
1149
|
+
if host_mode and (device_mode or config_mode or show_certs or wipe_certs or (gen_ip is not None)):
|
|
1150
|
+
print("ERROR: --host cannot be combined with device/config/cert commands. Run them separately.", file=sys.stderr)
|
|
1151
|
+
return 2
|
|
1152
|
+
|
|
1153
|
+
if host_force and host_token_create_id is None:
|
|
1154
|
+
print("ERROR: --force is only valid with --host --token-create <host_id>", file=sys.stderr)
|
|
1155
|
+
return 2
|
|
1156
|
+
|
|
1157
|
+
if host_mode:
|
|
1158
|
+
actions = int(host_token_create_id is not None) + int(host_delete_id is not None) + int(host_list) + int(host_wipe)
|
|
1159
|
+
if actions == 0:
|
|
1160
|
+
print_help()
|
|
1161
|
+
return 2
|
|
1162
|
+
if actions > 1:
|
|
1163
|
+
print("ERROR: host commands are mutually exclusive. Use exactly one of: --token-create, --delete, --list, --wipe", file=sys.stderr)
|
|
1164
|
+
return 2
|
|
1165
|
+
|
|
1166
|
+
if host_wipe and not yes:
|
|
1167
|
+
# mimic your other wipe confirmations
|
|
1168
|
+
try:
|
|
1169
|
+
if input("This will delete ALL host tokens. Type 'wipe' to confirm: ").strip().lower() != "wipe":
|
|
1170
|
+
print("Wipe aborted.")
|
|
1171
|
+
return 1
|
|
1172
|
+
except KeyboardInterrupt:
|
|
1173
|
+
print("\nCancelled.")
|
|
1174
|
+
return 1
|
|
1175
|
+
|
|
1176
|
+
if gist_mode and (device_mode or config_mode or host_mode or show_certs or wipe_certs or (gen_ip is not None)):
|
|
1177
|
+
print("ERROR: --gist cannot be combined with device/config/host/cert commands. Run them separately.", file=sys.stderr)
|
|
1178
|
+
return 2
|
|
1179
|
+
|
|
1180
|
+
# ----------------
|
|
1181
|
+
# Gist mode execution
|
|
1182
|
+
# ----------------
|
|
1183
|
+
if gist_mode:
|
|
1184
|
+
actions = int(gist_show) + int(gist_wipe) + int(gist_set)
|
|
1185
|
+
if actions == 0:
|
|
1186
|
+
print_help()
|
|
1187
|
+
return 2
|
|
1188
|
+
if actions > 1:
|
|
1189
|
+
print("ERROR: gist commands are mutually exclusive. Use exactly one of: --set, --show, --wipe", file=sys.stderr)
|
|
1190
|
+
return 2
|
|
1191
|
+
|
|
1192
|
+
if gist_show:
|
|
1193
|
+
return int(_gist_show(show_secrets=gist_show_secrets))
|
|
1194
|
+
|
|
1195
|
+
if gist_wipe:
|
|
1196
|
+
return int(_gist_wipe(yes=yes))
|
|
1197
|
+
|
|
1198
|
+
# gist_set
|
|
1199
|
+
if gist_id is None or gist_filename is None:
|
|
1200
|
+
print("ERROR: --gist --set requires --id <gist_id> and --file <filename>", file=sys.stderr)
|
|
1201
|
+
return 2
|
|
1202
|
+
|
|
1203
|
+
tok = gist_token or ""
|
|
1204
|
+
if gist_token_stdin:
|
|
1205
|
+
tok = sys.stdin.read().strip()
|
|
1206
|
+
if not tok:
|
|
1207
|
+
# secure prompt (no echo)
|
|
1208
|
+
tok = getpass.getpass("GitHub token (won't echo): ").strip()
|
|
1209
|
+
|
|
1210
|
+
return int(_gist_set(gist_id=gist_id, filename=gist_filename, token=tok))
|
|
1211
|
+
|
|
1212
|
+
# ----------------
|
|
1213
|
+
# Host mode execution
|
|
1214
|
+
# ----------------
|
|
1215
|
+
if host_mode:
|
|
1216
|
+
# 1) list
|
|
1217
|
+
if host_list:
|
|
1218
|
+
show_host_tokens(include_secrets=host_show_secrets, file=sys.stdout)
|
|
1219
|
+
return 0
|
|
1220
|
+
|
|
1221
|
+
# 2) delete
|
|
1222
|
+
if host_delete_id is not None:
|
|
1223
|
+
try:
|
|
1224
|
+
ok = delete_host_token(host_delete_id)
|
|
1225
|
+
except Exception as e:
|
|
1226
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1227
|
+
return 2
|
|
1228
|
+
|
|
1229
|
+
if ok:
|
|
1230
|
+
print(f"Deleted host token: {host_delete_id}")
|
|
1231
|
+
return 0
|
|
1232
|
+
else:
|
|
1233
|
+
print(f"No host token found for: {host_delete_id}")
|
|
1234
|
+
return 1
|
|
1235
|
+
|
|
1236
|
+
# 3) wipe all
|
|
1237
|
+
if host_wipe:
|
|
1238
|
+
try:
|
|
1239
|
+
n = wipe_all_host_tokens()
|
|
1240
|
+
except Exception as e:
|
|
1241
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1242
|
+
return 2
|
|
1243
|
+
print(f"Wiped {n} host token record(s).")
|
|
1244
|
+
return 0
|
|
1245
|
+
|
|
1246
|
+
# 4) token create
|
|
1247
|
+
if host_token_create_id is None:
|
|
1248
|
+
print_help()
|
|
1249
|
+
return 2
|
|
1250
|
+
|
|
1251
|
+
try:
|
|
1252
|
+
token_len = host_token_length if host_token_length is not None else 8
|
|
1253
|
+
token = create_host_token(host_token_create_id, length=token_len, overwrite=host_force)
|
|
1254
|
+
except Exception as e:
|
|
1255
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1256
|
+
return 2
|
|
1257
|
+
|
|
1258
|
+
print("Host token created and stored at ~/.config/remoterf/db/hosts_auth.env")
|
|
1259
|
+
print(f'To config host:\n hostrf --config --host {host_token_create_id} "{token}"')
|
|
1260
|
+
return 0
|
|
1261
|
+
|
|
1262
|
+
# ----------------
|
|
1263
|
+
# Device mode execution
|
|
1264
|
+
# ----------------
|
|
1265
|
+
if device_mode:
|
|
1266
|
+
if device_wipe and (
|
|
1267
|
+
device_show
|
|
1268
|
+
or device_add_spec is not None
|
|
1269
|
+
or device_remove_gid is not None
|
|
1270
|
+
or device_edit_gid is not None
|
|
1271
|
+
):
|
|
1272
|
+
print("ERROR: cannot combine device add/remove/show/edit with device wipe. Use '-d -w' only.", file=sys.stderr)
|
|
1273
|
+
return 2
|
|
1274
|
+
|
|
1275
|
+
if device_wipe:
|
|
1276
|
+
return int(_wipe_devices_only(yes=yes))
|
|
1277
|
+
|
|
1278
|
+
did_any = False
|
|
1279
|
+
|
|
1280
|
+
if device_show:
|
|
1281
|
+
rc = _device_show()
|
|
1282
|
+
if rc != 0:
|
|
1283
|
+
return rc
|
|
1284
|
+
did_any = True
|
|
1285
|
+
|
|
1286
|
+
if device_add_type == "pluto" and device_add_spec is not None:
|
|
1287
|
+
rc = _device_add_pluto(device_add_spec)
|
|
1288
|
+
if rc != 0:
|
|
1289
|
+
return rc
|
|
1290
|
+
did_any = True
|
|
1291
|
+
|
|
1292
|
+
if device_edit_gid is not None:
|
|
1293
|
+
rc = _device_edit_name(device_edit_gid, device_edit_new_name or "")
|
|
1294
|
+
if rc != 0:
|
|
1295
|
+
return rc
|
|
1296
|
+
did_any = True
|
|
1297
|
+
|
|
1298
|
+
if device_remove_gid is not None:
|
|
1299
|
+
rc = _device_remove(device_remove_gid)
|
|
1300
|
+
if rc != 0:
|
|
1301
|
+
return rc
|
|
1302
|
+
did_any = True
|
|
1303
|
+
|
|
1304
|
+
if not did_any:
|
|
1305
|
+
print_help()
|
|
1306
|
+
return 2
|
|
1307
|
+
|
|
1308
|
+
return 0
|
|
1309
|
+
|
|
1310
|
+
# ----------------
|
|
1311
|
+
# Config mode execution
|
|
1312
|
+
# ----------------
|
|
1313
|
+
if config_mode:
|
|
1314
|
+
if config_wipe and (config_show or main_port is not None or cert_port is not None):
|
|
1315
|
+
print("ERROR: cannot combine --wipe with --show or port. Use '-c -w' only.", file=sys.stderr)
|
|
1316
|
+
return 2
|
|
1317
|
+
|
|
1318
|
+
if config_wipe:
|
|
1319
|
+
return int(_config_wipe_ports(yes=yes))
|
|
1320
|
+
|
|
1321
|
+
did_any = False
|
|
1322
|
+
|
|
1323
|
+
if config_show:
|
|
1324
|
+
rc = _config_show_ports()
|
|
1325
|
+
if rc != 0:
|
|
1326
|
+
return rc
|
|
1327
|
+
did_any = True
|
|
1328
|
+
|
|
1329
|
+
if main_port is not None or cert_port is not None:
|
|
1330
|
+
rc = _config_set_ports(main_port=main_port, cert_port=cert_port)
|
|
1331
|
+
if rc != 0:
|
|
1332
|
+
return rc
|
|
1333
|
+
did_any = True
|
|
1334
|
+
|
|
1335
|
+
if not did_any:
|
|
1336
|
+
print_help()
|
|
1337
|
+
return 2
|
|
1338
|
+
|
|
1339
|
+
return 0
|
|
1340
|
+
|
|
1341
|
+
# ----------------
|
|
1342
|
+
# Cert mode execution
|
|
1343
|
+
# ----------------
|
|
1344
|
+
if wipe_certs and show_certs:
|
|
1345
|
+
print("ERROR: cannot combine --show-certs with --wipe-certs", file=sys.stderr)
|
|
1346
|
+
return 2
|
|
1347
|
+
|
|
1348
|
+
if wipe_certs:
|
|
1349
|
+
return int(_wipe_certs(yes=yes))
|
|
1350
|
+
|
|
1351
|
+
if show_certs and gen_ip is None:
|
|
1352
|
+
return int(_show_certs(verbose=verbose))
|
|
1353
|
+
|
|
1354
|
+
if gen_ip is None:
|
|
1355
|
+
print_help()
|
|
1356
|
+
return 2
|
|
1357
|
+
|
|
1358
|
+
rc = _gen_certs(
|
|
1359
|
+
static_ip=gen_ip,
|
|
1360
|
+
days=days,
|
|
1361
|
+
ca_days=ca_days,
|
|
1362
|
+
bits=bits,
|
|
1363
|
+
dns=dns,
|
|
1364
|
+
cn=cn,
|
|
1365
|
+
force=force,
|
|
1366
|
+
no_detect_ip=no_detect_ip,
|
|
1367
|
+
)
|
|
1368
|
+
if rc != 0:
|
|
1369
|
+
return rc
|
|
1370
|
+
|
|
1371
|
+
if show_certs:
|
|
1372
|
+
return int(_show_certs(verbose=verbose))
|
|
1373
|
+
|
|
1374
|
+
return 0
|
|
1375
|
+
|
|
1376
|
+
if __name__ == "__main__":
|
|
1377
|
+
raise SystemExit(main())
|