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.
Files changed (44) hide show
  1. remoteRF_server/__init__.py +0 -0
  2. remoteRF_server/common/__init__.py +0 -0
  3. remoteRF_server/common/grpc/__init__.py +1 -0
  4. remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
  5. remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
  6. remoteRF_server/common/grpc/grpc_pb2.py +59 -0
  7. remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
  8. remoteRF_server/common/idl/__init__.py +1 -0
  9. remoteRF_server/common/idl/device_schema.py +39 -0
  10. remoteRF_server/common/idl/pluto_schema.py +174 -0
  11. remoteRF_server/common/idl/schema.py +358 -0
  12. remoteRF_server/common/utils/__init__.py +6 -0
  13. remoteRF_server/common/utils/ansi_codes.py +120 -0
  14. remoteRF_server/common/utils/api_token.py +21 -0
  15. remoteRF_server/common/utils/db_connection.py +35 -0
  16. remoteRF_server/common/utils/db_location.py +24 -0
  17. remoteRF_server/common/utils/list_string.py +5 -0
  18. remoteRF_server/common/utils/process_arg.py +80 -0
  19. remoteRF_server/drivers/__init__.py +0 -0
  20. remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  21. remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
  22. remoteRF_server/host/__init__.py +0 -0
  23. remoteRF_server/host/host_auth_token.py +292 -0
  24. remoteRF_server/host/host_directory_store.py +142 -0
  25. remoteRF_server/host/host_tunnel_server.py +1388 -0
  26. remoteRF_server/server/__init__.py +0 -0
  27. remoteRF_server/server/acc_perms.py +317 -0
  28. remoteRF_server/server/cert_provider.py +184 -0
  29. remoteRF_server/server/device_manager.py +688 -0
  30. remoteRF_server/server/grpc_server.py +1023 -0
  31. remoteRF_server/server/reservation.py +811 -0
  32. remoteRF_server/server/rpc_manager.py +104 -0
  33. remoteRF_server/server/user_group_cli.py +723 -0
  34. remoteRF_server/server/user_group_handler.py +1120 -0
  35. remoteRF_server/serverrf_cli.py +1377 -0
  36. remoteRF_server/tools/__init__.py +191 -0
  37. remoteRF_server/tools/gen_certs.py +274 -0
  38. remoteRF_server/tools/gist_status.py +139 -0
  39. remoteRF_server/tools/gist_status_testing.py +67 -0
  40. remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
  41. remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
  42. remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
  43. remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
  44. remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,105 @@
1
+ from ...server.device_manager import set_pluto, get_device as device_pluto
2
+ from ...common.utils.process_arg import unmap_arg, map_arg
3
+
4
+ class PlutoServer:
5
+
6
+ @staticmethod
7
+ def handle_call(*, function_name, args):
8
+ try:
9
+ # print(f'fn {function_name}')
10
+
11
+ function_name_args = function_name.split(':')
12
+ function_name = function_name_args[1]
13
+ function_type = function_name_args[2]
14
+ if function_name == "ip":
15
+ set_pluto(ip=args[function_name].string_value.split(":")[1])
16
+ elif function_type == "GET":
17
+ return PlutoServer.get(function_name=function_name, token=unmap_arg(args['a']))
18
+ elif function_type == "SET":
19
+ PlutoServer.set(function_name=function_name, value=args[function_name], token=unmap_arg(args['a']))
20
+
21
+ elif function_type in ("CALL0", "CALL1"):
22
+ return PlutoServer.call_func(
23
+ function_name=function_name,
24
+ function_type=function_type,
25
+ args=args,
26
+ token=unmap_arg(args['a'])
27
+ )
28
+ else:
29
+ raise ValueError(f"Unknown function_type: {function_type}. Is that a function you can call?")
30
+ except Exception as e:
31
+ return {function_name:map_arg('None'), "Error":map_arg(f'{e}')}
32
+
33
+ @staticmethod
34
+ def call_func(*, function_name, function_type, args, token):
35
+ device = device_pluto(api_token=token)
36
+ if device is None:
37
+ return {function_name: map_arg('None'), "UE": map_arg("API Token Invalid")}
38
+
39
+ method = getattr(device, function_name)
40
+ if not callable(method):
41
+ raise ValueError(f"Device attribute '{function_name}' is not callable.")
42
+
43
+ # if PlutoServer.DEBUG:
44
+
45
+ # print(f"[DEBUG] call_func: calling {function_name} on {device} with type={function_type}")
46
+
47
+ # print(f"raw values: {args['arg1']}")
48
+
49
+ if function_type == "CALL0":
50
+ # Zero-argument function call
51
+ result = method()
52
+ elif function_type == "CALL1":
53
+ # Single-argument function call
54
+ if 'arg1' not in args:
55
+ raise ValueError("Client Package: CALL1 type requires 'arg1' in args.")
56
+ python_arg = unmap_arg(args['arg1'])
57
+ # if PlutoServer.DEBUG:
58
+ # print(f"[DEBUG] call_func: unmap_arg(args['arg1']) => {python_arg}")
59
+ # print(f"Pluto.{method} \n unmapped arg: {python_arg}")
60
+ # print(f"unmapped values: {python_arg}")
61
+ # print(f"{function_name} is called on {device}")
62
+ result = method(python_arg)
63
+ else:
64
+ raise ValueError(f"Unsupported call type: {function_type} (arg length not supported)")
65
+
66
+ # If the function returns something, map it back
67
+ if result is not None:
68
+ return {function_name: map_arg(result)}
69
+ else:
70
+ # If there's no return value, return some placeholder
71
+ return {function_name: map_arg('None')}
72
+
73
+ @staticmethod
74
+ def get(*, function_name, token):
75
+ try:
76
+ device = device_pluto(api_token=token)
77
+ if device == None:
78
+ return {function_name:map_arg('None'), "UE":map_arg("API Token Invalid")}
79
+ if not hasattr(device, function_name):
80
+ return {function_name:map_arg('None'), "UE":map_arg(f'{function_name} is not a gettable value.')}
81
+
82
+ attr = getattr(device, function_name)
83
+ if callable(attr):
84
+ return {function_name:map_arg(attr())}
85
+ else:
86
+ return {function_name:map_arg(attr)}
87
+ except Exception as e:
88
+
89
+ # print(f"UE: Pluto_server ln:35: {e}")
90
+ return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
91
+
92
+ @staticmethod
93
+ def set(*, function_name, value, token):
94
+ try:
95
+ device = device_pluto(api_token=token)
96
+ if device == None:
97
+ return {function_name:map_arg('None'), "UE":map_arg("API Token Invalid")}
98
+ if not hasattr(device, function_name):
99
+ return {function_name:map_arg('None'), "UE":map_arg(f'{function_name} is not a settable value.')}
100
+
101
+ setattr(device, function_name, unmap_arg(value))
102
+ return {}
103
+ except Exception as e:
104
+ # print(f"UE: Pluto_server ln:44: {e}")
105
+ return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
File without changes
@@ -0,0 +1,292 @@
1
+ # remoteRF_server/host/host_auth_token.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import sys
8
+ import time
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Dict, Tuple, List
12
+ from datetime import datetime, timezone
13
+ import hashlib
14
+
15
+ from ..common.utils import *
16
+
17
+ _HOST_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
18
+
19
+ def _validate_host_id(raw: str) -> str:
20
+ hid = (raw or "").strip()
21
+ if not hid:
22
+ raise ValueError("Missing host_id")
23
+ if hid == "unknown-host":
24
+ raise ValueError("host_id cannot be 'unknown-host'")
25
+ if not _HOST_ID_RE.fullmatch(hid):
26
+ raise ValueError("Invalid host_id (allowed: 1..64 chars of [A-Za-z0-9_.-])")
27
+ return hid
28
+
29
+ def _hosts_auth_path() -> Path:
30
+ return get_db_dir() / "hosts_auth.env"
31
+
32
+ def _utc_now_s() -> str:
33
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
34
+
35
+ def _host_key(host_id: str) -> str:
36
+ return hashlib.sha256(host_id.encode("utf-8")).hexdigest()[:16]
37
+
38
+ # Minimal env read/write
39
+
40
+ def _read_env_kv(path: Path) -> Dict[str, str]:
41
+ out: Dict[str, str] = {}
42
+ if not path.exists():
43
+ return out
44
+ for raw in path.read_text(encoding="utf-8").splitlines():
45
+ line = raw.strip()
46
+ if not line or line.startswith("#") or "=" not in line:
47
+ continue
48
+ k, v = line.split("=", 1)
49
+ out[k.strip()] = v.strip().strip('"').strip("'")
50
+ return out
51
+
52
+ def _write_env_kv(path: Path, kv: Dict[str, str]) -> None:
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ # deterministic output makes diffs/debugging easier
55
+ lines: List[str] = []
56
+ for k in sorted(kv.keys()):
57
+ v = str(kv[k])
58
+ if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
59
+ v = v.replace('"', '\\"')
60
+ lines.append(f'{k}="{v}"')
61
+ else:
62
+ lines.append(f"{k}={v}")
63
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
64
+
65
+ # Cached DB load (thread-safe)
66
+
67
+ _DB_LOCK = threading.RLock()
68
+ _DB_CACHE_MTIME: float = 0.0
69
+ _DB_CACHE: Dict[str, str] = {}
70
+
71
+ def _load_db_kv() -> Dict[str, str]:
72
+ global _DB_CACHE_MTIME, _DB_CACHE
73
+ p = _hosts_auth_path()
74
+
75
+ try:
76
+ st = p.stat()
77
+ mtime = float(st.st_mtime)
78
+ except FileNotFoundError:
79
+ with _DB_LOCK:
80
+ _DB_CACHE_MTIME = 0.0
81
+ _DB_CACHE = {}
82
+ return {}
83
+ except Exception:
84
+ # if stat fails oddly, fall back to best-effort read
85
+ mtime = -1.0
86
+
87
+ with _DB_LOCK:
88
+ if mtime > 0 and _DB_CACHE and _DB_CACHE_MTIME == mtime:
89
+ return dict(_DB_CACHE)
90
+
91
+ kv = _read_env_kv(p)
92
+ _DB_CACHE = dict(kv)
93
+ _DB_CACHE_MTIME = mtime if mtime > 0 else time.time()
94
+ return dict(kv)
95
+
96
+ def _save_db_kv(kv: Dict[str, str]) -> None:
97
+ global _DB_CACHE_MTIME, _DB_CACHE
98
+ p = _hosts_auth_path()
99
+ _write_env_kv(p, kv)
100
+ # refresh cache immediately
101
+ try:
102
+ mtime = float(p.stat().st_mtime)
103
+ except Exception:
104
+ mtime = time.time()
105
+ with _DB_LOCK:
106
+ _DB_CACHE = dict(kv)
107
+ _DB_CACHE_MTIME = mtime
108
+
109
+ # Public API
110
+
111
+ def create_host_token(
112
+ host_id: str,
113
+ *,
114
+ length: int = 8,
115
+ status: str = "approved",
116
+ overwrite: bool = True,
117
+ ) -> str:
118
+ hid = _validate_host_id(host_id)
119
+ hk = _host_key(hid)
120
+
121
+ salt, hashed, token = generate_token(length=length)
122
+ now = _utc_now_s()
123
+
124
+ kv = _load_db_kv()
125
+
126
+ # If not overwriting and host exists, refuse
127
+ if not overwrite and kv.get(f"HOST_{hk}_ID", "").strip():
128
+ raise ValueError(f"Host token already exists for host_id={hid!r} (use --force to overwrite)")
129
+
130
+ kv[f"HOST_{hk}_ID"] = hid
131
+ kv[f"HOST_{hk}_SALT"] = salt
132
+ kv[f"HOST_{hk}_HASH"] = hashed
133
+ kv[f"HOST_{hk}_STATUS"] = (status or "approved").strip().lower()
134
+ kv[f"HOST_{hk}_UPDATED_UTC"] = now
135
+ if f"HOST_{hk}_CREATED_UTC" not in kv:
136
+ kv[f"HOST_{hk}_CREATED_UTC"] = now
137
+
138
+ _save_db_kv(kv)
139
+ return token
140
+
141
+ def set_host_status(host_id: str, status: str) -> None:
142
+ hid = _validate_host_id(host_id)
143
+ hk = _host_key(hid)
144
+
145
+ kv = _load_db_kv()
146
+ if kv.get(f"HOST_{hk}_ID", "").strip() != hid:
147
+ raise ValueError(f"Unknown host_id={hid!r} (no record)")
148
+
149
+ kv[f"HOST_{hk}_STATUS"] = (status or "").strip().lower()
150
+ kv[f"HOST_{hk}_UPDATED_UTC"] = _utc_now_s()
151
+ _save_db_kv(kv)
152
+
153
+ def is_host_token_valid(host_id: str, token: str, *, require_status: str = "approved") -> bool:
154
+ try:
155
+ hid = _validate_host_id(host_id)
156
+ except Exception:
157
+ return False
158
+
159
+ tok = (token or "").strip()
160
+ if not tok:
161
+ return False
162
+
163
+ hk = _host_key(hid)
164
+ kv = _load_db_kv()
165
+
166
+ rid = (kv.get(f"HOST_{hk}_ID", "") or "").strip()
167
+ if rid != hid:
168
+ return False
169
+
170
+ status = (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower()
171
+ if require_status and status != require_status.strip().lower():
172
+ return False
173
+
174
+ salt = (kv.get(f"HOST_{hk}_SALT", "") or "").strip()
175
+ hashed = (kv.get(f"HOST_{hk}_HASH", "") or "").strip()
176
+ if not salt or not hashed:
177
+ return False
178
+
179
+ try:
180
+ return bool(validate_token(salt, hashed, tok))
181
+ except Exception:
182
+ return False
183
+
184
+ def list_hosts() -> List[Tuple[str, str]]:
185
+ kv = _load_db_kv()
186
+ out: List[Tuple[str, str]] = []
187
+ # Scan by HOST_<hk>_ID keys
188
+ for k, v in kv.items():
189
+ if not k.startswith("HOST_") or not k.endswith("_ID"):
190
+ continue
191
+ hk = k[len("HOST_") : -len("_ID")]
192
+ hid = (v or "").strip()
193
+ st = (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower()
194
+ if hid:
195
+ out.append((hid, st or ""))
196
+ out.sort(key=lambda t: t[0])
197
+ return out
198
+
199
+ def _host_record_keys(hk: str) -> list[str]:
200
+ # all keys stored for this host_key
201
+ return [
202
+ f"HOST_{hk}_ID",
203
+ f"HOST_{hk}_SALT",
204
+ f"HOST_{hk}_HASH",
205
+ f"HOST_{hk}_STATUS",
206
+ f"HOST_{hk}_CREATED_UTC",
207
+ f"HOST_{hk}_UPDATED_UTC",
208
+ ]
209
+
210
+ def list_host_token_records(*, include_secrets: bool = False) -> list[dict[str, str]]:
211
+ kv = _load_db_kv()
212
+ out: list[dict[str, str]] = []
213
+
214
+ # Find all host keys by scanning HOST_<hk>_ID
215
+ for k, v in kv.items():
216
+ if not k.startswith("HOST_") or not k.endswith("_ID"):
217
+ continue
218
+
219
+ hk = k[len("HOST_") : -len("_ID")]
220
+ host_id = (v or "").strip()
221
+ if not host_id:
222
+ continue
223
+
224
+ rec: dict[str, str] = {
225
+ "host_id": host_id,
226
+ "status": (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower(),
227
+ "created_utc": (kv.get(f"HOST_{hk}_CREATED_UTC", "") or "").strip(),
228
+ "updated_utc": (kv.get(f"HOST_{hk}_UPDATED_UTC", "") or "").strip(),
229
+ }
230
+
231
+ if include_secrets:
232
+ rec["salt"] = (kv.get(f"HOST_{hk}_SALT", "") or "").strip()
233
+ rec["hash"] = (kv.get(f"HOST_{hk}_HASH", "") or "").strip()
234
+
235
+ out.append(rec)
236
+
237
+ out.sort(key=lambda r: r.get("host_id", ""))
238
+ return out
239
+
240
+ def delete_host_token(host_id: str) -> bool:
241
+ hid = _validate_host_id(host_id)
242
+ hk = _host_key(hid)
243
+
244
+ kv = _load_db_kv()
245
+ rid = (kv.get(f"HOST_{hk}_ID", "") or "").strip()
246
+ if rid != hid:
247
+ return False
248
+
249
+ removed = False
250
+ for k in _host_record_keys(hk):
251
+ if k in kv:
252
+ kv.pop(k, None)
253
+ removed = True
254
+
255
+ if removed:
256
+ _save_db_kv(kv)
257
+ return removed
258
+
259
+ def wipe_all_host_tokens() -> int:
260
+ kv = _load_db_kv()
261
+
262
+ # Count how many hosts are present
263
+ host_keys: list[str] = []
264
+ for k in kv.keys():
265
+ if k.startswith("HOST_") and k.endswith("_ID"):
266
+ hk = k[len("HOST_") : -len("_ID")]
267
+ host_keys.append(hk)
268
+
269
+ # Remove all per-host keys
270
+ for hk in host_keys:
271
+ for kk in _host_record_keys(hk):
272
+ kv.pop(kk, None)
273
+
274
+ _save_db_kv(kv)
275
+ return len(host_keys)
276
+
277
+ def show_host_tokens(*, include_secrets: bool = False, file=sys.stdout) -> None:
278
+ recs = list_host_token_records(include_secrets=include_secrets)
279
+ if not recs:
280
+ print(f"No host tokens found ({_hosts_auth_path()}).", file=file)
281
+ return
282
+
283
+ print(f"Host tokens ({_hosts_auth_path()}):", file=file)
284
+ for r in recs:
285
+ host_id = r.get("host_id", "")
286
+ status = r.get("status", "")
287
+ created = r.get("created_utc", "")
288
+ updated = r.get("updated_utc", "")
289
+ print(f" - {host_id} status={status} created={created} updated={updated}", file=file)
290
+ if include_secrets:
291
+ print(f" salt={r.get('salt','')}", file=file)
292
+ print(f" hash={r.get('hash','')}", file=file)
@@ -0,0 +1,142 @@
1
+ # src/remoteRF_host/host/host_directory_store.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+
11
+ _ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
12
+
13
+ def now_ms() -> int:
14
+ import time
15
+ return int(time.time() * 1000)
16
+
17
+ def repo_root_from_file(file: str) -> Path:
18
+ p = Path(file).resolve()
19
+ for parent in p.parents:
20
+ if parent.name == "src":
21
+ return parent.parent
22
+ return p.parents[3]
23
+
24
+ def cfg_dir_from_file(file: str) -> Path:
25
+ return repo_root_from_file(file) / ".config"
26
+
27
+ def sanitize_env_key(s: str) -> str:
28
+ s = (s or "").strip().upper()
29
+ s = re.sub(r"[^A-Z0-9_]", "_", s)
30
+ s = re.sub(r"_+", "_", s)
31
+ return s[:120]
32
+
33
+ def strip_quotes(v: str) -> str:
34
+ v = (v or "").strip()
35
+ if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
36
+ return v[1:-1]
37
+ return v
38
+
39
+ def csv_split(v: str) -> List[str]:
40
+ return [x.strip() for x in (v or "").split(",") if x.strip()]
41
+
42
+ class DeviceIdConflictError(RuntimeError):
43
+ def __init__(self, *, device_id: str, existing_host: str, new_host: str) -> None:
44
+ super().__init__(
45
+ f"device_id '{device_id}' already owned by host '{existing_host}', rejecting host '{new_host}'"
46
+ )
47
+ self.device_id = device_id
48
+ self.existing_host = existing_host
49
+ self.new_host = new_host
50
+
51
+ class EnvStore:
52
+ def __init__(self, path: Path) -> None:
53
+ self.path = Path(path)
54
+ self._lock = threading.Lock()
55
+
56
+ def _read_lines(self) -> List[str]:
57
+ if not self.path.exists():
58
+ return []
59
+ return self.path.read_text(encoding="utf-8").splitlines()
60
+
61
+ def _write_lines(self, lines: List[str]) -> None:
62
+ self.path.parent.mkdir(parents=True, exist_ok=True)
63
+ self.path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
64
+
65
+ def read_kv(self) -> Dict[str, str]:
66
+ out: Dict[str, str] = {}
67
+ for raw in self._read_lines():
68
+ line = raw.strip()
69
+ if not line or line.startswith("#"):
70
+ continue
71
+ m = _ENV_LINE.match(line)
72
+ if not m:
73
+ continue
74
+ k = m.group(1).strip()
75
+ v = strip_quotes(m.group(2))
76
+ out[k] = v
77
+ return out
78
+
79
+ def append_to_csv_list(self, key: str, value: str) -> None:
80
+ key = sanitize_env_key(key)
81
+ value = (value or "").strip()
82
+ if not value:
83
+ return
84
+
85
+ with self._lock:
86
+ lines = self._read_lines()
87
+ idx: Optional[int] = None
88
+ cur = ""
89
+
90
+ for i, ln in enumerate(lines):
91
+ if ln.startswith(key + "="):
92
+ idx = i
93
+ cur = ln[len(key) + 1:].strip()
94
+ break
95
+
96
+ existing = csv_split(cur)
97
+ if value in existing:
98
+ return
99
+
100
+ existing.append(value)
101
+ new_line = f"{key}=" + ",".join(existing)
102
+
103
+ if idx is None:
104
+ lines.append(new_line)
105
+ else:
106
+ lines[idx] = new_line
107
+
108
+ self._write_lines(lines)
109
+
110
+ def upsert_kv(self, key: str, value: str) -> None:
111
+ key = sanitize_env_key(key)
112
+ value = (value or "").strip()
113
+
114
+ with self._lock:
115
+ lines = self._read_lines()
116
+ idx: Optional[int] = None
117
+ for i, ln in enumerate(lines):
118
+ if ln.startswith(key + "="):
119
+ idx = i
120
+ break
121
+
122
+ new_line = f"{key}={value}"
123
+ if idx is None:
124
+ lines.append(new_line)
125
+ else:
126
+ lines[idx] = new_line
127
+
128
+ self._write_lines(lines)
129
+
130
+ def set_kv_if_absent(self, key: str, value: str) -> None:
131
+ key = sanitize_env_key(key)
132
+ value = (value or "").strip()
133
+ if not value:
134
+ return
135
+
136
+ with self._lock:
137
+ lines = self._read_lines()
138
+ for ln in lines:
139
+ if ln.startswith(key + "="):
140
+ return
141
+ lines.append(f"{key}={value}")
142
+ self._write_lines(lines)