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,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import shutil
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run(cmd: list[str]) -> None:
|
|
14
|
+
try:
|
|
15
|
+
subprocess.run(cmd, check=True)
|
|
16
|
+
except subprocess.CalledProcessError as e:
|
|
17
|
+
print(f"\nCommand failed:\n {' '.join(cmd)}\n", file=sys.stderr)
|
|
18
|
+
raise SystemExit(e.returncode)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_openssl() -> None:
|
|
22
|
+
if shutil.which("openssl") is None:
|
|
23
|
+
raise SystemExit("openssl not found on PATH. Install OpenSSL and try again.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_config_dir() -> Path:
|
|
27
|
+
# ~/.config/remoterf (matches your cert_fetcher convention)
|
|
28
|
+
return Path.home() / ".config" / "remoterf"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _detect_local_ip() -> str:
|
|
32
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
33
|
+
try:
|
|
34
|
+
# No packets need to actually leave; this forces route selection
|
|
35
|
+
s.connect(("8.8.8.8", 80))
|
|
36
|
+
return s.getsockname()[0]
|
|
37
|
+
finally:
|
|
38
|
+
s.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
_ensure_openssl()
|
|
43
|
+
|
|
44
|
+
ap = argparse.ArgumentParser(
|
|
45
|
+
prog="RRRFcerts",
|
|
46
|
+
description="Generate a local CA and server TLS cert/key with proper SANs into ~/.config/remoterf/certs",
|
|
47
|
+
)
|
|
48
|
+
ap.add_argument("--config-dir", default=str(_default_config_dir()), help="Base config dir (default: ~/.config/remoterf)")
|
|
49
|
+
ap.add_argument("--out-subdir", default="certs", help="Subdir under config-dir (default: certs)")
|
|
50
|
+
ap.add_argument("--ip", action="append", default=[], help="IP to include in SAN (repeatable)")
|
|
51
|
+
ap.add_argument("--dns", action="append", default=[], help="DNS name to include in SAN (repeatable)")
|
|
52
|
+
ap.add_argument("--cn", default=None, help="Common Name (defaults to first SAN entry)")
|
|
53
|
+
ap.add_argument("--days", type=int, default=365, help="Server cert validity in days")
|
|
54
|
+
ap.add_argument("--ca-days", type=int, default=3650, help="CA cert validity in days")
|
|
55
|
+
ap.add_argument("--bits", type=int, default=2048, help="RSA key size")
|
|
56
|
+
ap.add_argument("--force", action="store_true", help="Overwrite existing certs/keys")
|
|
57
|
+
ap.add_argument("--no-detect-ip", action="store_true", help="Do not auto-detect IP when none provided")
|
|
58
|
+
args = ap.parse_args()
|
|
59
|
+
|
|
60
|
+
base_dir = Path(args.config_dir).expanduser().resolve()
|
|
61
|
+
out_dir = (base_dir / args.out_subdir).resolve()
|
|
62
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
# If no SANs specified, auto-detect local IP unless disabled
|
|
65
|
+
ips = [x.strip() for x in args.ip if x and x.strip()]
|
|
66
|
+
dns = [x.strip() for x in args.dns if x and x.strip()]
|
|
67
|
+
|
|
68
|
+
if not ips and not dns and not args.no_detect_ip:
|
|
69
|
+
ips = [_detect_local_ip()]
|
|
70
|
+
|
|
71
|
+
if not ips and not dns:
|
|
72
|
+
raise SystemExit("Provide at least one --ip and/or --dns (or omit --no-detect-ip).")
|
|
73
|
+
|
|
74
|
+
sans: list[str] = [f"IP:{ip}" for ip in ips] + [f"DNS:{d}" for d in dns]
|
|
75
|
+
san_line = ",".join(sans)
|
|
76
|
+
cn = args.cn or (dns[0] if dns else ips[0])
|
|
77
|
+
|
|
78
|
+
# Filenames
|
|
79
|
+
ca_key = out_dir / "ca.key"
|
|
80
|
+
ca_crt = out_dir / "ca.crt"
|
|
81
|
+
|
|
82
|
+
srv_key = out_dir / "server.key"
|
|
83
|
+
srv_csr = out_dir / "server.csr"
|
|
84
|
+
srv_crt = out_dir / "server.crt"
|
|
85
|
+
|
|
86
|
+
targets = [ca_key, ca_crt, srv_key, srv_csr, srv_crt]
|
|
87
|
+
|
|
88
|
+
if not args.force:
|
|
89
|
+
existing = [p for p in targets if p.exists()]
|
|
90
|
+
if existing:
|
|
91
|
+
raise SystemExit(
|
|
92
|
+
"Refusing to overwrite existing files:\n "
|
|
93
|
+
+ "\n ".join(str(p) for p in existing)
|
|
94
|
+
+ "\nRe-run with --force to replace them."
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
for p in targets:
|
|
98
|
+
if p.exists():
|
|
99
|
+
p.unlink()
|
|
100
|
+
|
|
101
|
+
# openssl may have left-over serial files from previous runs
|
|
102
|
+
for extra in (out_dir / "ca.srl",):
|
|
103
|
+
if extra.exists():
|
|
104
|
+
extra.unlink()
|
|
105
|
+
|
|
106
|
+
# 1) CA key + self-signed cert
|
|
107
|
+
_run(["openssl", "genrsa", "-out", str(ca_key), str(args.bits)])
|
|
108
|
+
_run([
|
|
109
|
+
"openssl", "req", "-x509", "-new", "-nodes",
|
|
110
|
+
"-key", str(ca_key),
|
|
111
|
+
"-sha256",
|
|
112
|
+
"-days", str(args.ca_days),
|
|
113
|
+
"-subj", "/CN=remoteRF Local CA",
|
|
114
|
+
"-out", str(ca_crt),
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
# 2) Server key
|
|
118
|
+
_run(["openssl", "genrsa", "-out", str(srv_key), str(args.bits)])
|
|
119
|
+
|
|
120
|
+
# 3) CSR with SANs, then sign with CA including v3 extensions
|
|
121
|
+
with tempfile.TemporaryDirectory() as td:
|
|
122
|
+
td_path = Path(td)
|
|
123
|
+
|
|
124
|
+
csr_cnf = td_path / "csr.cnf"
|
|
125
|
+
csr_cnf.write_text(
|
|
126
|
+
f"""\
|
|
127
|
+
[ req ]
|
|
128
|
+
default_bits = {args.bits}
|
|
129
|
+
prompt = no
|
|
130
|
+
default_md = sha256
|
|
131
|
+
distinguished_name = dn
|
|
132
|
+
req_extensions = req_ext
|
|
133
|
+
|
|
134
|
+
[ dn ]
|
|
135
|
+
CN = {cn}
|
|
136
|
+
|
|
137
|
+
[ req_ext ]
|
|
138
|
+
subjectAltName = {san_line}
|
|
139
|
+
keyUsage = critical, digitalSignature, keyEncipherment
|
|
140
|
+
extendedKeyUsage = serverAuth
|
|
141
|
+
""",
|
|
142
|
+
encoding="utf-8",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
_run([
|
|
146
|
+
"openssl", "req", "-new",
|
|
147
|
+
"-key", str(srv_key),
|
|
148
|
+
"-out", str(srv_csr),
|
|
149
|
+
"-config", str(csr_cnf),
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
sign_cnf = td_path / "sign.cnf"
|
|
153
|
+
sign_cnf.write_text(
|
|
154
|
+
f"""\
|
|
155
|
+
[ v3_req ]
|
|
156
|
+
basicConstraints = CA:FALSE
|
|
157
|
+
subjectAltName = {san_line}
|
|
158
|
+
keyUsage = critical, digitalSignature, keyEncipherment
|
|
159
|
+
extendedKeyUsage = serverAuth
|
|
160
|
+
""",
|
|
161
|
+
encoding="utf-8",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
_run([
|
|
165
|
+
"openssl", "x509", "-req",
|
|
166
|
+
"-in", str(srv_csr),
|
|
167
|
+
"-CA", str(ca_crt),
|
|
168
|
+
"-CAkey", str(ca_key),
|
|
169
|
+
"-CAcreateserial",
|
|
170
|
+
"-out", str(srv_crt),
|
|
171
|
+
"-days", str(args.days),
|
|
172
|
+
"-sha256",
|
|
173
|
+
"-extfile", str(sign_cnf),
|
|
174
|
+
"-extensions", "v3_req",
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
print("\nGenerated certs/keys:")
|
|
178
|
+
print(f" Output dir: {out_dir}")
|
|
179
|
+
print(f" CA cert: {ca_crt}")
|
|
180
|
+
print(f" Server cert:{srv_crt}")
|
|
181
|
+
print(f" Server key: {srv_key}")
|
|
182
|
+
print("\nSANs:")
|
|
183
|
+
for s in sans:
|
|
184
|
+
print(f" - {s}")
|
|
185
|
+
print("\nClient trust:")
|
|
186
|
+
print(" Clients should trust ca.crt (NOT server.crt).")
|
|
187
|
+
print(" Clients must dial an address that matches one of the SANs.\n")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
def _x509_dates(cert_path: Path) -> tuple[datetime | None, datetime | None]:
|
|
14
|
+
# openssl prints:
|
|
15
|
+
# notBefore=Feb 16 23:12:02 2026 GMT
|
|
16
|
+
# notAfter=Feb 16 23:12:02 2027 GMT
|
|
17
|
+
p = subprocess.run(
|
|
18
|
+
["openssl", "x509", "-in", str(cert_path), "-noout", "-startdate", "-enddate"],
|
|
19
|
+
stdout=subprocess.PIPE,
|
|
20
|
+
stderr=subprocess.STDOUT,
|
|
21
|
+
text=True,
|
|
22
|
+
check=False,
|
|
23
|
+
)
|
|
24
|
+
if p.returncode != 0:
|
|
25
|
+
return None, None
|
|
26
|
+
|
|
27
|
+
nb = na = None
|
|
28
|
+
for line in p.stdout.splitlines():
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if line.startswith("notBefore="):
|
|
31
|
+
s = line.split("=", 1)[1].strip()
|
|
32
|
+
s = s.removesuffix(" GMT").strip()
|
|
33
|
+
try:
|
|
34
|
+
nb = datetime.strptime(s, "%b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc)
|
|
35
|
+
except Exception:
|
|
36
|
+
nb = None
|
|
37
|
+
elif line.startswith("notAfter="):
|
|
38
|
+
s = line.split("=", 1)[1].strip()
|
|
39
|
+
s = s.removesuffix(" GMT").strip()
|
|
40
|
+
try:
|
|
41
|
+
na = datetime.strptime(s, "%b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc)
|
|
42
|
+
except Exception:
|
|
43
|
+
na = None
|
|
44
|
+
|
|
45
|
+
return nb, na
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _run(cmd: list[str]) -> None:
|
|
49
|
+
try:
|
|
50
|
+
subprocess.run(cmd, check=True)
|
|
51
|
+
except subprocess.CalledProcessError as e:
|
|
52
|
+
print(f"\nCommand failed:\n {' '.join(cmd)}\n", file=sys.stderr)
|
|
53
|
+
raise SystemExit(e.returncode)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_openssl() -> None:
|
|
57
|
+
if shutil.which("openssl") is None:
|
|
58
|
+
raise SystemExit("openssl not found on PATH. Install OpenSSL and try again.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _default_config_dir() -> Path:
|
|
62
|
+
return Path.home() / ".config" / "remoterf"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _detect_local_ip() -> str:
|
|
66
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
67
|
+
try:
|
|
68
|
+
s.connect(("8.8.8.8", 80))
|
|
69
|
+
return s.getsockname()[0]
|
|
70
|
+
finally:
|
|
71
|
+
s.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def generate_certs(
|
|
75
|
+
*,
|
|
76
|
+
static_ip: str | None,
|
|
77
|
+
days: int = 365,
|
|
78
|
+
ca_days: int = 3650,
|
|
79
|
+
bits: int = 2048,
|
|
80
|
+
config_dir: Path | None = None,
|
|
81
|
+
out_subdir: str = "certs",
|
|
82
|
+
dns: list[str] | None = None,
|
|
83
|
+
cn: str | None = None,
|
|
84
|
+
force: bool = False,
|
|
85
|
+
) -> None:
|
|
86
|
+
_ensure_openssl()
|
|
87
|
+
|
|
88
|
+
base_dir = (config_dir or _default_config_dir()).expanduser().resolve()
|
|
89
|
+
out_dir = (base_dir / out_subdir).resolve()
|
|
90
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
ips: list[str] = []
|
|
93
|
+
if static_ip and static_ip.strip():
|
|
94
|
+
ips.append(static_ip.strip())
|
|
95
|
+
else:
|
|
96
|
+
ips.append(_detect_local_ip())
|
|
97
|
+
|
|
98
|
+
dns = [d.strip() for d in (dns or []) if d and d.strip()]
|
|
99
|
+
sans: list[str] = [f"IP:{ip}" for ip in ips] + [f"DNS:{d}" for d in dns]
|
|
100
|
+
san_line = ",".join(sans)
|
|
101
|
+
cn = cn or (dns[0] if dns else ips[0])
|
|
102
|
+
|
|
103
|
+
ca_key = out_dir / "ca.key"
|
|
104
|
+
ca_crt = out_dir / "ca.crt"
|
|
105
|
+
srv_key = out_dir / "server.key"
|
|
106
|
+
srv_csr = out_dir / "server.csr"
|
|
107
|
+
srv_crt = out_dir / "server.crt"
|
|
108
|
+
|
|
109
|
+
targets = [ca_key, ca_crt, srv_key, srv_csr, srv_crt]
|
|
110
|
+
|
|
111
|
+
if not force:
|
|
112
|
+
existing = [p for p in targets if p.exists()]
|
|
113
|
+
if existing:
|
|
114
|
+
raise SystemExit(
|
|
115
|
+
"Refusing to overwrite existing files:\n "
|
|
116
|
+
+ "\n ".join(str(p) for p in existing)
|
|
117
|
+
+ "\nRe-run with --force to replace them."
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
for p in targets:
|
|
121
|
+
if p.exists():
|
|
122
|
+
p.unlink()
|
|
123
|
+
extra = out_dir / "ca.srl"
|
|
124
|
+
if extra.exists():
|
|
125
|
+
extra.unlink()
|
|
126
|
+
|
|
127
|
+
_run(["openssl", "genrsa", "-out", str(ca_key), str(bits)])
|
|
128
|
+
_run([
|
|
129
|
+
"openssl", "req", "-x509", "-new", "-nodes",
|
|
130
|
+
"-key", str(ca_key),
|
|
131
|
+
"-sha256",
|
|
132
|
+
"-days", str(ca_days),
|
|
133
|
+
"-subj", "/CN=remoteRF Local CA",
|
|
134
|
+
"-out", str(ca_crt),
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
_run(["openssl", "genrsa", "-out", str(srv_key), str(bits)])
|
|
138
|
+
|
|
139
|
+
with tempfile.TemporaryDirectory() as td:
|
|
140
|
+
td_path = Path(td)
|
|
141
|
+
|
|
142
|
+
csr_cnf = td_path / "csr.cnf"
|
|
143
|
+
csr_cnf.write_text(
|
|
144
|
+
f"""\
|
|
145
|
+
[ req ]
|
|
146
|
+
default_bits = {bits}
|
|
147
|
+
prompt = no
|
|
148
|
+
default_md = sha256
|
|
149
|
+
distinguished_name = dn
|
|
150
|
+
req_extensions = req_ext
|
|
151
|
+
|
|
152
|
+
[ dn ]
|
|
153
|
+
CN = {cn}
|
|
154
|
+
|
|
155
|
+
[ req_ext ]
|
|
156
|
+
subjectAltName = {san_line}
|
|
157
|
+
keyUsage = critical, digitalSignature, keyEncipherment
|
|
158
|
+
extendedKeyUsage = serverAuth
|
|
159
|
+
""",
|
|
160
|
+
encoding="utf-8",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
_run([
|
|
164
|
+
"openssl", "req", "-new",
|
|
165
|
+
"-key", str(srv_key),
|
|
166
|
+
"-out", str(srv_csr),
|
|
167
|
+
"-config", str(csr_cnf),
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
sign_cnf = td_path / "sign.cnf"
|
|
171
|
+
sign_cnf.write_text(
|
|
172
|
+
f"""\
|
|
173
|
+
[ v3_req ]
|
|
174
|
+
basicConstraints = CA:FALSE
|
|
175
|
+
subjectAltName = {san_line}
|
|
176
|
+
keyUsage = critical, digitalSignature, keyEncipherment
|
|
177
|
+
extendedKeyUsage = serverAuth
|
|
178
|
+
""",
|
|
179
|
+
encoding="utf-8",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
_run([
|
|
183
|
+
"openssl", "x509", "-req",
|
|
184
|
+
"-in", str(srv_csr),
|
|
185
|
+
"-CA", str(ca_crt),
|
|
186
|
+
"-CAkey", str(ca_key),
|
|
187
|
+
"-CAcreateserial",
|
|
188
|
+
"-out", str(srv_crt),
|
|
189
|
+
"-days", str(days),
|
|
190
|
+
"-sha256",
|
|
191
|
+
"-extfile", str(sign_cnf),
|
|
192
|
+
"-extensions", "v3_req",
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
print("\nGenerated certs/keys:")
|
|
196
|
+
print(f" Output dir: {out_dir}")
|
|
197
|
+
print(f" CA cert: {ca_crt}")
|
|
198
|
+
print(f" Server cert:{srv_crt}")
|
|
199
|
+
print(f" Server key: {srv_key}")
|
|
200
|
+
print("\nSANs:")
|
|
201
|
+
for s in sans:
|
|
202
|
+
print(f" - {s}")
|
|
203
|
+
print("\nClient trust:")
|
|
204
|
+
print(" Clients should trust ca.crt (NOT server.crt).")
|
|
205
|
+
print(" Clients must dial an address that matches one of the SANs.\n")
|
|
206
|
+
|
|
207
|
+
ca_nb, ca_na = _x509_dates(ca_crt)
|
|
208
|
+
srv_nb, srv_na = _x509_dates(srv_crt)
|
|
209
|
+
|
|
210
|
+
if srv_nb and srv_na:
|
|
211
|
+
now = datetime.now(timezone.utc)
|
|
212
|
+
remaining = srv_na - now
|
|
213
|
+
# clamp negative (if clock skew or already expired)
|
|
214
|
+
if remaining.total_seconds() < 0:
|
|
215
|
+
rem_str = "EXPIRED"
|
|
216
|
+
else:
|
|
217
|
+
days_left = remaining.days
|
|
218
|
+
hours_left = (remaining.seconds // 3600)
|
|
219
|
+
mins_left = (remaining.seconds % 3600) // 60
|
|
220
|
+
rem_str = f"{days_left}d {hours_left}h {mins_left}m"
|
|
221
|
+
|
|
222
|
+
print(" Server:")
|
|
223
|
+
print(f" notBefore (UTC): {srv_nb.isoformat(timespec='seconds')}")
|
|
224
|
+
print(f" notAfter (UTC): {srv_na.isoformat(timespec='seconds')}")
|
|
225
|
+
print(f" expires (local): {srv_na.astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
|
226
|
+
print(f" expires in: {rem_str}")
|
|
227
|
+
else:
|
|
228
|
+
print(" Server: (failed to parse dates)")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
import argparse
|
|
233
|
+
|
|
234
|
+
def main() -> int:
|
|
235
|
+
ap = argparse.ArgumentParser(prog="gen_certs.py", description="Generate RemoteRF TLS certs")
|
|
236
|
+
ap.add_argument("--ip", dest="static_ip", default=None, help="Static IP to include in SAN (if omitted, auto-detect unless --no-detect-ip)")
|
|
237
|
+
ap.add_argument("--no-detect-ip", action="store_true", help="Require --ip; do not auto-detect")
|
|
238
|
+
|
|
239
|
+
ap.add_argument("--days", type=int, default=365)
|
|
240
|
+
ap.add_argument("--ca-days", type=int, default=3650)
|
|
241
|
+
ap.add_argument("--bits", type=int, default=2048)
|
|
242
|
+
|
|
243
|
+
ap.add_argument("--config-dir", default=None, help="Base config dir (default: ~/.config/remoterf)")
|
|
244
|
+
ap.add_argument("--out-subdir", default="certs", help="Subdir under config dir (default: certs)")
|
|
245
|
+
|
|
246
|
+
ap.add_argument("--dns", action="append", default=[], help="DNS SAN entry (repeatable)")
|
|
247
|
+
ap.add_argument("--cn", default=None, help="Common Name (default: first SAN entry)")
|
|
248
|
+
ap.add_argument("--force", action="store_true", help="Overwrite existing certs/keys")
|
|
249
|
+
|
|
250
|
+
args = ap.parse_args()
|
|
251
|
+
|
|
252
|
+
if args.no_detect_ip and not (args.static_ip and args.static_ip.strip()):
|
|
253
|
+
print("ERROR: --no-detect-ip set but no --ip provided", file=sys.stderr)
|
|
254
|
+
return 2
|
|
255
|
+
|
|
256
|
+
cfg = Path(args.config_dir).expanduser().resolve() if args.config_dir else None
|
|
257
|
+
|
|
258
|
+
generate_certs(
|
|
259
|
+
static_ip=args.static_ip,
|
|
260
|
+
days=args.days,
|
|
261
|
+
ca_days=args.ca_days,
|
|
262
|
+
bits=args.bits,
|
|
263
|
+
config_dir=cfg,
|
|
264
|
+
out_subdir=args.out_subdir,
|
|
265
|
+
dns=args.dns,
|
|
266
|
+
cn=args.cn,
|
|
267
|
+
force=args.force,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# remoteRF_server/tools/gist_status.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _xdg_config_home() -> Path:
|
|
17
|
+
return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cfg_dir() -> Path:
|
|
21
|
+
return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_env_kv(path: Path) -> Dict[str, str]:
|
|
25
|
+
out: Dict[str, str] = {}
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return out
|
|
28
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
29
|
+
line = raw.strip()
|
|
30
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
31
|
+
continue
|
|
32
|
+
k, v = line.split("=", 1)
|
|
33
|
+
out[k.strip()] = v.strip().strip('"').strip("'")
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_gist_env() -> tuple[str, str, str]:
|
|
38
|
+
p = _cfg_dir() / "gist.env"
|
|
39
|
+
kv = _read_env_kv(p)
|
|
40
|
+
|
|
41
|
+
gist_id = kv.get("STATUS_GIST_ID", "").strip()
|
|
42
|
+
token = kv.get("GITHUB_TOKEN", "").strip()
|
|
43
|
+
filename = kv.get("STATUS_GIST_FILENAME", "").strip()
|
|
44
|
+
|
|
45
|
+
missing = []
|
|
46
|
+
if not gist_id:
|
|
47
|
+
missing.append("STATUS_GIST_ID")
|
|
48
|
+
if not token:
|
|
49
|
+
missing.append("GITHUB_TOKEN")
|
|
50
|
+
if not filename:
|
|
51
|
+
missing.append("STATUS_GIST_FILENAME")
|
|
52
|
+
|
|
53
|
+
if missing:
|
|
54
|
+
print(f"[gist_status] Missing {', '.join(missing)} in {p}", file=sys.stderr)
|
|
55
|
+
print(
|
|
56
|
+
"[gist_status] Fix: run:\n"
|
|
57
|
+
" serverrf --gist --set --id <gist_id> --file <filename>\n"
|
|
58
|
+
"Or manually create gist.env with those keys.",
|
|
59
|
+
file=sys.stderr,
|
|
60
|
+
)
|
|
61
|
+
raise SystemExit(2)
|
|
62
|
+
|
|
63
|
+
return gist_id, token, filename
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_status() -> dict:
|
|
67
|
+
payload = {
|
|
68
|
+
"updated_unix": int(time.time()),
|
|
69
|
+
"reservation_history_days": 90,
|
|
70
|
+
"reservations": [],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
from ..server.reservation import reservation_handler
|
|
74
|
+
payload["reservations"] = reservation_handler.get_obfuscated_device_reservations_grouped_past_days(90)
|
|
75
|
+
payload["usage"] = reservation_handler.get_obfuscated_usage_summary()
|
|
76
|
+
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def push_gist(*, gist_id: str, gh_token: str, filename: str, payload: dict) -> None:
|
|
81
|
+
url = f"https://api.github.com/gists/{gist_id}"
|
|
82
|
+
headers = {
|
|
83
|
+
"Authorization": f"Bearer {gh_token}",
|
|
84
|
+
"Accept": "application/vnd.github+json",
|
|
85
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
86
|
+
}
|
|
87
|
+
body = {
|
|
88
|
+
"files": {
|
|
89
|
+
filename: {
|
|
90
|
+
"content": json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
r = requests.patch(url, headers=headers, json=body, timeout=10)
|
|
95
|
+
r.raise_for_status()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _publisher_loop(period_sec: int) -> None:
|
|
99
|
+
gist_id, gh_token, filename = _load_gist_env()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
push_gist(gist_id=gist_id, gh_token=gh_token, filename=filename, payload=build_status())
|
|
103
|
+
print("[gist_status] initial publish ok", file=sys.stderr)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"[gist_status] initial publish failed: {e}", file=sys.stderr)
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
try:
|
|
109
|
+
push_gist(gist_id=gist_id, gh_token=gh_token, filename=filename, payload=build_status())
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"[gist_status] publish failed: {e}", file=sys.stderr)
|
|
112
|
+
time.sleep(period_sec)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_publisher_thread: Optional[threading.Thread] = None
|
|
116
|
+
_publisher_lock = threading.Lock()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def start_status_publisher() -> None:
|
|
120
|
+
global _publisher_thread
|
|
121
|
+
with _publisher_lock:
|
|
122
|
+
if _publisher_thread is not None and _publisher_thread.is_alive():
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
t = threading.Thread(
|
|
126
|
+
target=_publisher_loop,
|
|
127
|
+
args=(int(120),),
|
|
128
|
+
name="gist-status-publisher",
|
|
129
|
+
daemon=True,
|
|
130
|
+
)
|
|
131
|
+
t.start()
|
|
132
|
+
_publisher_thread = t
|
|
133
|
+
print(f"[gist_status] started (period={120}s)", file=sys.stderr)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# if __name__ == "__main__":
|
|
137
|
+
# start_status_publisher(period_sec=120)
|
|
138
|
+
# while True:
|
|
139
|
+
# time.sleep(3600)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# remoteRF_server/tools/gist_status.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
# Create public gist on your github. Keep note of the file name
|
|
10
|
+
# https://gist.github.com/ethange1/2a35e08a90bf88a70dfe7f42a55685ed
|
|
11
|
+
|
|
12
|
+
# The last one is the "GIST_ID" part of the URL, e.g. "2a35e08a90bf88a70dfe7f42a55685ed"
|
|
13
|
+
|
|
14
|
+
GIST_ID = "2a35e08a90bf88a70dfe7f42a55685ed"
|
|
15
|
+
|
|
16
|
+
# Create the GITHUB_TOKEN (so your server can update it)
|
|
17
|
+
# GitHub → Settings
|
|
18
|
+
# Developer settings
|
|
19
|
+
# Personal access tokens → Fine-grained tokens
|
|
20
|
+
# Generate new token
|
|
21
|
+
# Set repository access to "Public Respositories"
|
|
22
|
+
# Permissions: enable Gists: Read and write
|
|
23
|
+
# Generate + copy the token (you only see it once)
|
|
24
|
+
|
|
25
|
+
# LIMIT OF 1 per minute ! BE MINDFUL OF RATE LIMITS!
|
|
26
|
+
|
|
27
|
+
GH_TOKEN = ""
|
|
28
|
+
|
|
29
|
+
FILENAME = "ucla-wlab-remoterf-status.json"
|
|
30
|
+
|
|
31
|
+
# HOW TO PULL! https://gist.githubusercontent.com/ethange1/2a35e08a90bf88a70dfe7f42a55685ed/raw/ucla-wlab-remoterf-status.json
|
|
32
|
+
|
|
33
|
+
def build_status() -> dict:
|
|
34
|
+
return {
|
|
35
|
+
"updated_unix": int(time.time()),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def push_gist(payload: dict) -> None:
|
|
39
|
+
url = f"https://api.github.com/gists/{GIST_ID}"
|
|
40
|
+
headers = {
|
|
41
|
+
"Authorization": f"Bearer {GH_TOKEN}",
|
|
42
|
+
"Accept": "application/vnd.github+json",
|
|
43
|
+
}
|
|
44
|
+
body = {
|
|
45
|
+
"files": {
|
|
46
|
+
FILENAME: {
|
|
47
|
+
"content": json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
r = requests.patch(url, headers=headers, json=body, timeout=10)
|
|
52
|
+
print(r.status_code, r.text)
|
|
53
|
+
print("Retry-After:", r.headers.get("Retry-After"))
|
|
54
|
+
print("X-RateLimit-Remaining:", r.headers.get("X-RateLimit-Remaining"))
|
|
55
|
+
r.raise_for_status()
|
|
56
|
+
|
|
57
|
+
def status_publisher_loop(period_sec: int = 30) -> None:
|
|
58
|
+
while True:
|
|
59
|
+
try:
|
|
60
|
+
push_gist(build_status())
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"[status] publish failed: {e}")
|
|
63
|
+
time.sleep(period_sec)
|
|
64
|
+
|
|
65
|
+
status_publisher_loop(60)
|
|
66
|
+
|
|
67
|
+
# threading.Thread(target=status_publisher_loop, daemon=True).start()
|