RemoteRF 2.0.15__py3-none-any.whl → 2.1.0.dev7__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/common/grpc/__init__.py +1 -1
- remoteRF/common/grpc/deployment_capabilities.proto +15 -0
- remoteRF/common/grpc/deployment_capabilities_pb2.py +41 -0
- remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +100 -0
- remoteRF/common/grpc/v2_codec.py +1 -1
- remoteRF/common/utils/banner.py +13 -10
- remoteRF/common/utils/process_arg.py +31 -2
- remoteRF/config/cert_fetcher.py +30 -5
- remoteRF/config/config.py +84 -7
- remoteRF/core/acc_login.py +11 -4
- remoteRF/core/app.py +930 -228
- remoteRF/core/direct_path.py +707 -0
- remoteRF/core/grpc_acc.py +96 -16
- remoteRF/core/grpc_client.py +200 -50
- remoteRF/core/secure_channel.py +63 -0
- remoteRF/deployment/__init__.py +1 -0
- remoteRF/deployment/backend.py +416 -0
- remoteRF/deployment/direct.py +38 -0
- remoteRF/deployment/homes.py +383 -0
- remoteRF/deployment/http.py +121 -0
- remoteRF/deployment/state.py +187 -0
- remoteRF/drivers/adalm_pluto/__init__.py +6 -1
- remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +388 -0
- remoteRF/drivers/dynamic_device.py +50 -46
- remoteRF/drivers/dynamic_v2.py +38 -25
- remoteRF/drivers/sim_sdr/__init__.py +21 -0
- remoteRF/drivers/sim_sdr/sim_sdr_remote.py +183 -0
- remoteRF/remoterf_cli.py +328 -26
- {remoterf-2.0.15.dist-info → remoterf-2.1.0.dev7.dist-info}/METADATA +84 -3
- remoterf-2.1.0.dev7.dist-info/RECORD +67 -0
- remoterf-2.0.15.dist-info/RECORD +0 -53
- {remoterf-2.0.15.dist-info → remoterf-2.1.0.dev7.dist-info}/WHEEL +0 -0
- {remoterf-2.0.15.dist-info → remoterf-2.1.0.dev7.dist-info}/entry_points.txt +0 -0
- {remoterf-2.0.15.dist-info → remoterf-2.1.0.dev7.dist-info}/licenses/LICENSE +0 -0
- {remoterf-2.0.15.dist-info → remoterf-2.1.0.dev7.dist-info}/top_level.txt +0 -0
remoteRF/common/grpc/__init__.py
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
package remote_rf;
|
|
3
|
+
import "grpc.proto";
|
|
4
|
+
|
|
5
|
+
// Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
6
|
+
service DeploymentCapabilitiesV1 {
|
|
7
|
+
rpc GetCapabilities (DeploymentCapabilitiesRequest) returns (DeploymentCapabilitiesResponse);
|
|
8
|
+
}
|
|
9
|
+
message DeploymentCapabilitiesRequest {
|
|
10
|
+
string protocol_version = 1;
|
|
11
|
+
}
|
|
12
|
+
message DeploymentCapabilitiesResponse {
|
|
13
|
+
string capabilities_json = 1;
|
|
14
|
+
ErrorEnvelope error = 2;
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: deployment_capabilities.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'deployment_capabilities.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from . import grpc_pb2 as grpc__pb2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x64\x65ployment_capabilities.proto\x12\tremote_rf\x1a\ngrpc.proto\"9\n\x1d\x44\x65ploymentCapabilitiesRequest\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\"d\n\x1e\x44\x65ploymentCapabilitiesResponse\x12\x19\n\x11\x63\x61pabilities_json\x18\x01 \x01(\t\x12\'\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x18.remote_rf.ErrorEnvelope2\x82\x01\n\x18\x44\x65ploymentCapabilitiesV1\x12\x66\n\x0fGetCapabilities\x12(.remote_rf.DeploymentCapabilitiesRequest\x1a).remote_rf.DeploymentCapabilitiesResponseb\x06proto3')
|
|
29
|
+
|
|
30
|
+
_globals = globals()
|
|
31
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
32
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'deployment_capabilities_pb2', _globals)
|
|
33
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
34
|
+
DESCRIPTOR._loaded_options = None
|
|
35
|
+
_globals['_DEPLOYMENTCAPABILITIESREQUEST']._serialized_start=56
|
|
36
|
+
_globals['_DEPLOYMENTCAPABILITIESREQUEST']._serialized_end=113
|
|
37
|
+
_globals['_DEPLOYMENTCAPABILITIESRESPONSE']._serialized_start=115
|
|
38
|
+
_globals['_DEPLOYMENTCAPABILITIESRESPONSE']._serialized_end=215
|
|
39
|
+
_globals['_DEPLOYMENTCAPABILITIESV1']._serialized_start=218
|
|
40
|
+
_globals['_DEPLOYMENTCAPABILITIESV1']._serialized_end=348
|
|
41
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from . import deployment_capabilities_pb2 as deployment__capabilities__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.74.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ f' but the generated code in deployment_capabilities_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeploymentCapabilitiesV1Stub(object):
|
|
29
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, channel):
|
|
33
|
+
"""Constructor.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
channel: A grpc.Channel.
|
|
37
|
+
"""
|
|
38
|
+
self.GetCapabilities = channel.unary_unary(
|
|
39
|
+
'/remote_rf.DeploymentCapabilitiesV1/GetCapabilities',
|
|
40
|
+
request_serializer=deployment__capabilities__pb2.DeploymentCapabilitiesRequest.SerializeToString,
|
|
41
|
+
response_deserializer=deployment__capabilities__pb2.DeploymentCapabilitiesResponse.FromString,
|
|
42
|
+
_registered_method=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DeploymentCapabilitiesV1Servicer(object):
|
|
46
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def GetCapabilities(self, request, context):
|
|
50
|
+
"""Missing associated documentation comment in .proto file."""
|
|
51
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
52
|
+
context.set_details('Method not implemented!')
|
|
53
|
+
raise NotImplementedError('Method not implemented!')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_DeploymentCapabilitiesV1Servicer_to_server(servicer, server):
|
|
57
|
+
rpc_method_handlers = {
|
|
58
|
+
'GetCapabilities': grpc.unary_unary_rpc_method_handler(
|
|
59
|
+
servicer.GetCapabilities,
|
|
60
|
+
request_deserializer=deployment__capabilities__pb2.DeploymentCapabilitiesRequest.FromString,
|
|
61
|
+
response_serializer=deployment__capabilities__pb2.DeploymentCapabilitiesResponse.SerializeToString,
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
65
|
+
'remote_rf.DeploymentCapabilitiesV1', rpc_method_handlers)
|
|
66
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
67
|
+
server.add_registered_method_handlers('remote_rf.DeploymentCapabilitiesV1', rpc_method_handlers)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# This class is part of an EXPERIMENTAL API.
|
|
71
|
+
class DeploymentCapabilitiesV1(object):
|
|
72
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def GetCapabilities(request,
|
|
77
|
+
target,
|
|
78
|
+
options=(),
|
|
79
|
+
channel_credentials=None,
|
|
80
|
+
call_credentials=None,
|
|
81
|
+
insecure=False,
|
|
82
|
+
compression=None,
|
|
83
|
+
wait_for_ready=None,
|
|
84
|
+
timeout=None,
|
|
85
|
+
metadata=None):
|
|
86
|
+
return grpc.experimental.unary_unary(
|
|
87
|
+
request,
|
|
88
|
+
target,
|
|
89
|
+
'/remote_rf.DeploymentCapabilitiesV1/GetCapabilities',
|
|
90
|
+
deployment__capabilities__pb2.DeploymentCapabilitiesRequest.SerializeToString,
|
|
91
|
+
deployment__capabilities__pb2.DeploymentCapabilitiesResponse.FromString,
|
|
92
|
+
options,
|
|
93
|
+
channel_credentials,
|
|
94
|
+
insecure,
|
|
95
|
+
call_credentials,
|
|
96
|
+
compression,
|
|
97
|
+
wait_for_ready,
|
|
98
|
+
timeout,
|
|
99
|
+
metadata,
|
|
100
|
+
_registered_method=True)
|
remoteRF/common/grpc/v2_codec.py
CHANGED
|
@@ -48,7 +48,7 @@ def _json_safe(value):
|
|
|
48
48
|
if hasattr(value, "as_payload"):
|
|
49
49
|
return _json_safe(value.as_payload())
|
|
50
50
|
if isinstance(value, np.generic):
|
|
51
|
-
return
|
|
51
|
+
return value.item()
|
|
52
52
|
if isinstance(value, complex):
|
|
53
53
|
return {
|
|
54
54
|
_JSON_TYPE_KEY: "complex",
|
remoteRF/common/utils/banner.py
CHANGED
|
@@ -47,7 +47,7 @@ COMPACT_BANNER = "RemoteRF"
|
|
|
47
47
|
ASCII_WORDMARK_WIDTH = max(len(line) for line in ASCII_BANNER.splitlines())
|
|
48
48
|
ASCII_BANNER_WIDTH = max(len(line) for line in BRANDED_ASCII_BANNER.splitlines())
|
|
49
49
|
ASCII_BANNER_MIN_COLUMNS = ASCII_BANNER_WIDTH + 1
|
|
50
|
-
PANEL_CONTENT_WIDTH =
|
|
50
|
+
PANEL_CONTENT_WIDTH = 39
|
|
51
51
|
PANEL_INNER_WIDTH = PANEL_CONTENT_WIDTH + 2
|
|
52
52
|
PANEL_WIDTH = PANEL_INNER_WIDTH + 2
|
|
53
53
|
PANEL_GAP = 1
|
|
@@ -111,12 +111,15 @@ def _panel_lines(
|
|
|
111
111
|
marker = "◆" if unicode_supported else "*"
|
|
112
112
|
label = f"{horizontal} REMOTERF CLIENT "
|
|
113
113
|
top = f"{top_left}{label}{horizontal * (PANEL_INNER_WIDTH - len(label))}{top_right}"
|
|
114
|
-
content =
|
|
114
|
+
content = [
|
|
115
115
|
f"{marker} www.remoterf.net",
|
|
116
|
-
"Created by
|
|
116
|
+
"Created by https://wireless.ee.ucla.edu",
|
|
117
117
|
f"VERSION {version}",
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
]
|
|
119
|
+
# Before a target is chosen there is no server to name; an "NA" row would
|
|
120
|
+
# only say so twice.
|
|
121
|
+
if server:
|
|
122
|
+
content.append(f"SERVER {server}")
|
|
120
123
|
body = [
|
|
121
124
|
f"{vertical} {_fit_panel_text(line):<{PANEL_CONTENT_WIDTH}} {vertical}"
|
|
122
125
|
for line in content
|
|
@@ -169,12 +172,12 @@ def _compact_lines(
|
|
|
169
172
|
heading = COMPACT_BANNER[:width]
|
|
170
173
|
|
|
171
174
|
short_version = str(version).split(maxsplit=1)[0]
|
|
172
|
-
server = str(server or "
|
|
175
|
+
server = str(server or "")
|
|
173
176
|
detail_options = (
|
|
174
|
-
f"v{version} {separator} {server}",
|
|
175
|
-
|
|
176
|
-
f"v{
|
|
177
|
-
)
|
|
177
|
+
(f"v{version} {separator} {server}", f"v{short_version} {separator} {server}")
|
|
178
|
+
if server
|
|
179
|
+
else (f"v{version}",)
|
|
180
|
+
) + (f"v{short_version}",)
|
|
178
181
|
details = next((line for line in detail_options if len(line) <= width), "")
|
|
179
182
|
if not details:
|
|
180
183
|
details = detail_options[-1][:width]
|
|
@@ -30,6 +30,31 @@ _MAX_NDARRAY_DIMENSIONS = 8
|
|
|
30
30
|
_SUPPORTED_NDARRAY_KINDS = {"b", "i", "u", "f", "c"}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
# Gate I slice 3: complex samples as interleaved int16 IQ, a quarter of the
|
|
34
|
+
# complex128 bytes, decoded to complex64. Only exact for integer-valued data
|
|
35
|
+
# within int16 range (a 12-bit ADC's, say); map_iq16 refuses anything else.
|
|
36
|
+
IQ16 = "ci16"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fits_iq16(array: np.ndarray) -> bool:
|
|
40
|
+
if not (isinstance(array, np.ndarray) and np.iscomplexobj(array)):
|
|
41
|
+
return False
|
|
42
|
+
parts = np.stack((array.real, array.imag))
|
|
43
|
+
return bool(np.all(np.abs(parts) <= 32767) and np.array_equal(parts, np.round(parts)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def map_iq16(array: np.ndarray) -> grpc_pb2.Argument:
|
|
47
|
+
if not fits_iq16(array):
|
|
48
|
+
raise ValueError("samples are not integer-valued int16 IQ")
|
|
49
|
+
iq = np.empty(array.shape + (2,), dtype=np.int16)
|
|
50
|
+
iq[..., 0], iq[..., 1] = array.real, array.imag
|
|
51
|
+
arg = grpc_pb2.Argument()
|
|
52
|
+
arg.ndarray_value.dtype = IQ16
|
|
53
|
+
arg.ndarray_value.shape.extend(array.shape)
|
|
54
|
+
arg.ndarray_value.data = np.ascontiguousarray(iq).tobytes(order="C")
|
|
55
|
+
return arg
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
def _validate_ndarray(dtype: np.dtype, shape) -> int:
|
|
34
59
|
"""Return the expected payload length for a safe numeric ndarray."""
|
|
35
60
|
if dtype.hasobject or dtype.kind not in _SUPPORTED_NDARRAY_KINDS:
|
|
@@ -49,18 +74,22 @@ def _validate_ndarray(dtype: np.dtype, shape) -> int:
|
|
|
49
74
|
|
|
50
75
|
|
|
51
76
|
def _decode_ndarray(descriptor: grpc_pb2.NDArrayValue) -> np.ndarray:
|
|
77
|
+
iq16 = descriptor.dtype == IQ16
|
|
52
78
|
try:
|
|
53
|
-
dtype = np.dtype(descriptor.dtype)
|
|
79
|
+
dtype = np.dtype(np.int16) if iq16 else np.dtype(descriptor.dtype)
|
|
54
80
|
except (TypeError, ValueError) as exc:
|
|
55
81
|
raise ValueError(f"invalid ndarray dtype: {descriptor.dtype!r}") from exc
|
|
56
82
|
|
|
57
83
|
shape = tuple(int(item) for item in descriptor.shape)
|
|
58
|
-
expected = _validate_ndarray(dtype, shape)
|
|
84
|
+
expected = _validate_ndarray(dtype, shape + (2,) if iq16 else shape)
|
|
59
85
|
actual = len(descriptor.data)
|
|
60
86
|
if actual != expected:
|
|
61
87
|
raise ValueError(
|
|
62
88
|
f"ndarray byte length mismatch: expected {expected}, got {actual}"
|
|
63
89
|
)
|
|
90
|
+
if iq16:
|
|
91
|
+
iq = np.frombuffer(descriptor.data, dtype=dtype).reshape(shape + (2,))
|
|
92
|
+
return (iq[..., 0].astype(np.float32) + 1j * iq[..., 1].astype(np.float32)).astype(np.complex64)
|
|
64
93
|
return np.frombuffer(descriptor.data, dtype=dtype).reshape(shape).copy()
|
|
65
94
|
|
|
66
95
|
|
remoteRF/config/cert_fetcher.py
CHANGED
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import os
|
|
22
22
|
import socket
|
|
23
|
+
import urllib.error
|
|
23
24
|
import urllib.request
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Optional
|
|
@@ -38,6 +39,26 @@ def _ensure_parent_dir(p: Path) -> None:
|
|
|
38
39
|
def _looks_like_pem_cert(data: bytes) -> bool:
|
|
39
40
|
return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
|
|
40
41
|
|
|
42
|
+
|
|
43
|
+
def looks_like_pem_cert(data: bytes) -> bool:
|
|
44
|
+
"""Public alias of `_looks_like_pem_cert` for generic callers."""
|
|
45
|
+
return _looks_like_pem_cert(data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_ca_bytes(host: str, port: int, *, timeout_sec: float = 3.0) -> bytes:
|
|
49
|
+
"""Fetch raw CA certificate bytes (HTTP first, then raw-TCP fallback),
|
|
50
|
+
without writing anything to disk. Shared by direct-mode's
|
|
51
|
+
`fetch_and_save_ca_cert` below.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
return _fetch_http(host, port, timeout_sec)
|
|
55
|
+
except urllib.error.URLError as exc:
|
|
56
|
+
if isinstance(exc.reason, (OSError, socket.timeout)):
|
|
57
|
+
raise # nothing answered: a raw retry would only wait the timeout again
|
|
58
|
+
return _fetch_raw_tcp(host, port, timeout_sec)
|
|
59
|
+
except Exception:
|
|
60
|
+
return _fetch_raw_tcp(host, port, timeout_sec)
|
|
61
|
+
|
|
41
62
|
def sha256_fingerprint_pem(pem_bytes: bytes) -> str:
|
|
42
63
|
h = hashlib.sha256(pem_bytes).hexdigest()
|
|
43
64
|
return ":".join(h[i:i+2] for i in range(0, len(h), 2))
|
|
@@ -48,9 +69,16 @@ def _fetch_http(host: str, port: int, timeout_sec: float) -> bytes:
|
|
|
48
69
|
with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
|
|
49
70
|
return resp.read()
|
|
50
71
|
|
|
72
|
+
def _bare_host(host: str) -> str:
|
|
73
|
+
"""Strip IPv6 literal brackets (e.g. '[::1]' -> '::1') for raw-socket use."""
|
|
74
|
+
host = host.strip()
|
|
75
|
+
if host.startswith("[") and host.endswith("]"):
|
|
76
|
+
return host[1:-1]
|
|
77
|
+
return host
|
|
78
|
+
|
|
51
79
|
def _fetch_raw_tcp(host: str, port: int, timeout_sec: float) -> bytes:
|
|
52
80
|
chunks: list[bytes] = []
|
|
53
|
-
with socket.create_connection((host, port), timeout=timeout_sec) as s:
|
|
81
|
+
with socket.create_connection((_bare_host(host), port), timeout=timeout_sec) as s:
|
|
54
82
|
s.settimeout(timeout_sec)
|
|
55
83
|
while True:
|
|
56
84
|
try:
|
|
@@ -164,10 +192,7 @@ def fetch_and_save_ca_cert(
|
|
|
164
192
|
return True
|
|
165
193
|
|
|
166
194
|
# Fetch (HTTP first, then raw TCP fallback)
|
|
167
|
-
|
|
168
|
-
data = _fetch_http(host, port, timeout_sec)
|
|
169
|
-
except Exception:
|
|
170
|
-
data = _fetch_raw_tcp(host, port, timeout_sec)
|
|
195
|
+
data = fetch_ca_bytes(host, port, timeout_sec=timeout_sec)
|
|
171
196
|
|
|
172
197
|
if not data or not _looks_like_pem_cert(data):
|
|
173
198
|
return False
|
remoteRF/config/config.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import os
|
|
21
|
+
import socket
|
|
21
22
|
import sys
|
|
22
23
|
import shutil
|
|
23
24
|
from pathlib import Path
|
|
@@ -68,6 +69,27 @@ def _parse_hostport(s: str) -> Tuple[str, int]:
|
|
|
68
69
|
raise ValueError("Port out of range")
|
|
69
70
|
return host, port
|
|
70
71
|
|
|
72
|
+
|
|
73
|
+
def _bare_host(host: str) -> str:
|
|
74
|
+
"""Strip IPv6 literal brackets (e.g. '[::1]' -> '::1') for name/address
|
|
75
|
+
resolution and raw-socket use. IPv4 addresses and DNS hostnames
|
|
76
|
+
(including plain multi-label names such as ucla.global.remoterf.net)
|
|
77
|
+
are returned unchanged.
|
|
78
|
+
"""
|
|
79
|
+
host = host.strip()
|
|
80
|
+
if host.startswith("[") and host.endswith("]"):
|
|
81
|
+
return host[1:-1]
|
|
82
|
+
return host
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _host_resolves(host: str) -> bool:
|
|
86
|
+
"""Return True if `host` (an IPv4/IPv6 literal or DNS hostname) resolves."""
|
|
87
|
+
try:
|
|
88
|
+
socket.getaddrinfo(_bare_host(host), None)
|
|
89
|
+
return True
|
|
90
|
+
except socket.gaierror:
|
|
91
|
+
return False
|
|
92
|
+
|
|
71
93
|
def _write_env_kv(path: Path, kv: dict[str, str]) -> None:
|
|
72
94
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
95
|
lines: list[str] = []
|
|
@@ -151,21 +173,57 @@ def _print_config_summary(host: str, grpc_port: int, cert_port: int, ca_out: Pat
|
|
|
151
173
|
_print_separator()
|
|
152
174
|
|
|
153
175
|
|
|
154
|
-
def _confirm_tos(
|
|
176
|
+
def _confirm_tos(question: str = "Continue with configuration? [y/N]: ",
|
|
177
|
+
cancelled: str = "Configuration cancelled.") -> bool:
|
|
155
178
|
_print_separator()
|
|
156
179
|
_print_tos_notice()
|
|
157
180
|
_print_separator()
|
|
158
181
|
try:
|
|
159
|
-
reply = input(
|
|
160
|
-
except KeyboardInterrupt:
|
|
161
|
-
print("\
|
|
162
|
-
return False
|
|
163
|
-
except EOFError:
|
|
164
|
-
print("\nConfiguration cancelled.")
|
|
182
|
+
reply = input(question).strip().lower()
|
|
183
|
+
except (KeyboardInterrupt, EOFError):
|
|
184
|
+
print(f"\n{cancelled}")
|
|
165
185
|
return False
|
|
166
186
|
print()
|
|
167
187
|
return reply in {"y", "yes"}
|
|
168
188
|
|
|
189
|
+
|
|
190
|
+
# The one remembered agreement, for logins. A registration always asks anew
|
|
191
|
+
# and forgets this first, so a declined or abandoned `-r` means the next
|
|
192
|
+
# login asks again.
|
|
193
|
+
def _tos_agreement_path():
|
|
194
|
+
from ..deployment.state import root
|
|
195
|
+
|
|
196
|
+
return root() / "tos-agreed.json"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def tos_agreed() -> bool:
|
|
200
|
+
return _tos_agreement_path().exists()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def remember_tos_agreement() -> None:
|
|
204
|
+
from datetime import datetime, timezone
|
|
205
|
+
|
|
206
|
+
from ..deployment.state import private_write
|
|
207
|
+
|
|
208
|
+
private_write(_tos_agreement_path(), {"agreed_at": datetime.now(timezone.utc).isoformat()})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def forget_tos_agreement() -> None:
|
|
212
|
+
try:
|
|
213
|
+
_tos_agreement_path().unlink()
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def require_tos_for_login() -> bool:
|
|
219
|
+
"""Ask once, then remember -- until a registration clears it."""
|
|
220
|
+
if tos_agreed():
|
|
221
|
+
return True
|
|
222
|
+
if not _confirm_tos("Do you agree to the RemoteRF Terms of Service? [y/N]: ", "Login cancelled."):
|
|
223
|
+
return False
|
|
224
|
+
remember_tos_agreement()
|
|
225
|
+
return True
|
|
226
|
+
|
|
169
227
|
def configure(host: str, port: int, cert_port: int) -> int:
|
|
170
228
|
# Basic validation
|
|
171
229
|
host = (host or "").strip()
|
|
@@ -176,6 +234,16 @@ def configure(host: str, port: int, cert_port: int) -> int:
|
|
|
176
234
|
print("Error: port out of range", file=sys.stderr)
|
|
177
235
|
return 2
|
|
178
236
|
|
|
237
|
+
if not _host_resolves(host):
|
|
238
|
+
print(
|
|
239
|
+
"Error: Could not resolve RemoteRF server hostname:\n"
|
|
240
|
+
f" {host}\n"
|
|
241
|
+
"Check the address and your network/DNS connection, then re-run:\n"
|
|
242
|
+
" remoterf --config --addr <host:port>",
|
|
243
|
+
file=sys.stderr,
|
|
244
|
+
)
|
|
245
|
+
return 1
|
|
246
|
+
|
|
179
247
|
grpc_port = int(port)
|
|
180
248
|
cert_port = int(cert_port)
|
|
181
249
|
|
|
@@ -208,6 +276,15 @@ def configure(host: str, port: int, cert_port: int) -> int:
|
|
|
208
276
|
"REMOTERF_CA_CERT": str(ca_out),
|
|
209
277
|
"REMOTERF_PROFILE": profile,
|
|
210
278
|
})
|
|
279
|
+
# This profile is what the "default" home was adopted from. A new address
|
|
280
|
+
# or a re-fetched CA makes that adoption stale (its pin would now refuse
|
|
281
|
+
# this very server), so drop it and let the next login adopt this one.
|
|
282
|
+
try:
|
|
283
|
+
from ..deployment import homes
|
|
284
|
+
|
|
285
|
+
homes.forget_home(profile)
|
|
286
|
+
except Exception: # noqa: BLE001 - never fail configuration over bookkeeping
|
|
287
|
+
pass
|
|
211
288
|
|
|
212
289
|
_print_config_summary(host, grpc_port, cert_port, ca_out, env_file)
|
|
213
290
|
|
remoteRF/core/acc_login.py
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
def main(*, register=False, enrollment_code=None, server_label=None, show_banner=True,
|
|
17
|
+
home=None, route=None):
|
|
18
|
+
from .app import run
|
|
19
|
+
return run(
|
|
20
|
+
register=register,
|
|
21
|
+
enrollment_code=enrollment_code,
|
|
22
|
+
server_label=server_label,
|
|
23
|
+
show_banner=show_banner,
|
|
24
|
+
home=home,
|
|
25
|
+
route=route,
|
|
26
|
+
)
|