remoteRF-server 1.1.1__tar.gz → 1.1.2__tar.gz
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-1.1.1 → remoterf_server-1.1.2}/PKG-INFO +1 -1
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/pyproject.toml +1 -1
- {remoterf_server-1.1.1/src/remoteRF_server/drivers/adalm_pluto → remoterf_server-1.1.2/src/remoteRF_server/common/idl}/pluto_schema.py +3 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/api_token.py +19 -1
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +1 -2
- {remoterf_server-1.1.1/src/remoteRF_server/common/idl → remoterf_server-1.1.2/src/remoteRF_server/drivers/adalm_pluto}/pluto_schema.py +3 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/poe.py +62 -16
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/commands.py +1 -5
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/device_manager.py +12 -2
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/direct_path.py +69 -13
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/global_onboarding.py +45 -22
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/grpc_server.py +1 -1
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/reservation.py +98 -16
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/rpc_manager.py +14 -4
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/SOURCES.txt +2 -0
- remoterf_server-1.1.2/tests/test_account_hardening.py +134 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_cluster.py +1 -1
- remoterf_server-1.1.2/tests/test_direct_path_server.py +174 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_global_accounts_migration.py +3 -1
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_global_onboarding.py +98 -30
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_pluto_compat.py +1 -1
- remoterf_server-1.1.2/tests/test_pluto_poe.py +209 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_rpc_manager_calln.py +11 -3
- remoterf_server-1.1.1/tests/test_pluto_poe.py +0 -100
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/LICENSE +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/README.md +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/setup.cfg +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cluster.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/devices.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/enrollment.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/migrations.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/permissions.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/principals.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_client_sessions.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_commands.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_console.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_first_run.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_panel.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_status_push.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_usrp_uhd410_hardware.py +0 -0
- {remoterf_server-1.1.1 → remoterf_server-1.1.2}/tests/test_wipe.py +0 -0
|
@@ -202,6 +202,9 @@ class PlutoSchema(DeviceSchema):
|
|
|
202
202
|
|
|
203
203
|
@idl_expose(kind="set")
|
|
204
204
|
def set_rx_buffer_size(self, value):
|
|
205
|
+
# pyadi reads rx_buffer_size only when it (re)creates the RX buffer.
|
|
206
|
+
if value != self.device.rx_buffer_size:
|
|
207
|
+
self.device.rx_destroy_buffer()
|
|
205
208
|
self.device.rx_buffer_size = value
|
|
206
209
|
|
|
207
210
|
@idl_expose(kind="call")
|
{remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/api_token.py
RENAMED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import os
|
|
17
17
|
import hashlib
|
|
18
|
+
import hmac
|
|
18
19
|
import base64
|
|
19
20
|
import secrets
|
|
20
21
|
from dotenv import load_dotenv, find_dotenv
|
|
@@ -33,4 +34,21 @@ def validate_token(salt, hash, token) -> bool:
|
|
|
33
34
|
def hash_token(token: str) -> tuple[str, str]:
|
|
34
35
|
salt = os.urandom(16).hex()
|
|
35
36
|
hashed = hashlib.sha256(bytes.fromhex(salt) + token.encode()).hexdigest()
|
|
36
|
-
return salt, hashed
|
|
37
|
+
return salt, hashed
|
|
38
|
+
|
|
39
|
+
# Passwords get a real KDF; tokens stay on hash_token (random, high entropy).
|
|
40
|
+
PASSWORD_HASH_PREFIX = "pbkdf2$"
|
|
41
|
+
_PBKDF2_ROUNDS = 210_000
|
|
42
|
+
|
|
43
|
+
def hash_password(password: str) -> tuple[str, str]:
|
|
44
|
+
salt = os.urandom(16).hex()
|
|
45
|
+
digest = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), _PBKDF2_ROUNDS)
|
|
46
|
+
return salt, f"{PASSWORD_HASH_PREFIX}{_PBKDF2_ROUNDS}${digest.hex()}"
|
|
47
|
+
|
|
48
|
+
def verify_password(salt, hash, password) -> tuple[bool, bool]:
|
|
49
|
+
"""Returns (matches, needs_rehash). Accepts legacy hash_token() hashes."""
|
|
50
|
+
if not hash.startswith(PASSWORD_HASH_PREFIX):
|
|
51
|
+
return validate_token(salt, hash, password), True
|
|
52
|
+
_, rounds, digest = hash.split('$', 2)
|
|
53
|
+
computed = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), int(rounds))
|
|
54
|
+
return hmac.compare_digest(computed.hex(), digest), False
|
|
@@ -90,8 +90,7 @@ class PlutoServer:
|
|
|
90
90
|
if result is not None:
|
|
91
91
|
return {function_name: map_arg(result)}
|
|
92
92
|
else:
|
|
93
|
-
|
|
94
|
-
return {function_name: map_arg('None')}
|
|
93
|
+
return {function_name: map_arg(None)}
|
|
95
94
|
|
|
96
95
|
@staticmethod
|
|
97
96
|
def get(*, function_name, token):
|
|
@@ -202,6 +202,9 @@ class PlutoSchema(DeviceSchema):
|
|
|
202
202
|
|
|
203
203
|
@idl_expose(kind="set")
|
|
204
204
|
def set_rx_buffer_size(self, value):
|
|
205
|
+
# pyadi reads rx_buffer_size only when it (re)creates the RX buffer.
|
|
206
|
+
if value != self.device.rx_buffer_size:
|
|
207
|
+
self.device.rx_destroy_buffer()
|
|
205
208
|
self.device.rx_buffer_size = value
|
|
206
209
|
|
|
207
210
|
@idl_expose(kind="call")
|
{remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/poe.py
RENAMED
|
@@ -26,9 +26,11 @@ import threading
|
|
|
26
26
|
import time
|
|
27
27
|
|
|
28
28
|
from remoteRF_server.common.utils import get_remoterf_root
|
|
29
|
+
from remoteRF_server.common.utils.ansi_codes import Sty, printf
|
|
29
30
|
|
|
30
31
|
IIOD_PORT = 30431
|
|
31
|
-
BOOT_TIMEOUT_S =
|
|
32
|
+
BOOT_TIMEOUT_S = 45
|
|
33
|
+
LINK_CHECK_S = 30 # no IIOD by then: ask the switch whether the port even has a link
|
|
32
34
|
OFF_S = 3 # long enough for the Pluto to lose power before it is switched back on
|
|
33
35
|
|
|
34
36
|
|
|
@@ -90,6 +92,19 @@ def set_poe(*, switch, user, password, port, on):
|
|
|
90
92
|
ros.close()
|
|
91
93
|
|
|
92
94
|
|
|
95
|
+
def port_running(*, switch, user, password, port):
|
|
96
|
+
"""Whether the switch port has an Ethernet link (RouterOS 'running')."""
|
|
97
|
+
ros = RouterOS(switch, user, password)
|
|
98
|
+
try:
|
|
99
|
+
replies = ros.cmd("/interface/ethernet/print", f"?name={port}", "=.proplist=name,running")
|
|
100
|
+
finally:
|
|
101
|
+
ros.close()
|
|
102
|
+
for sentence in replies:
|
|
103
|
+
if sentence[0] == "!re":
|
|
104
|
+
return "=running=true" in sentence
|
|
105
|
+
raise RuntimeError(f"switch has no interface {port}")
|
|
106
|
+
|
|
107
|
+
|
|
93
108
|
def _iiod_up(ip):
|
|
94
109
|
try:
|
|
95
110
|
socket.create_connection((ip, IIOD_PORT), timeout=1).close()
|
|
@@ -110,6 +125,7 @@ class PoePluto:
|
|
|
110
125
|
_serial=str(serial or "").strip(),
|
|
111
126
|
_switch={"switch": switch, "user": user, "password": password},
|
|
112
127
|
_pluto=None,
|
|
128
|
+
fault=None, # message of the last failed boot; None once a boot succeeds
|
|
113
129
|
_lock=threading.RLock(),
|
|
114
130
|
)
|
|
115
131
|
|
|
@@ -119,28 +135,58 @@ class PoePluto:
|
|
|
119
135
|
def _poe(self, on):
|
|
120
136
|
set_poe(port=self._port, on=on, **self._switch)
|
|
121
137
|
|
|
138
|
+
@property
|
|
139
|
+
def power_state(self):
|
|
140
|
+
if self.fault:
|
|
141
|
+
return "fault"
|
|
142
|
+
return "on" if self._pluto is not None else "standby"
|
|
143
|
+
|
|
122
144
|
def power_up(self):
|
|
123
145
|
with self._lock:
|
|
124
146
|
if self._pluto is not None:
|
|
125
147
|
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
try:
|
|
149
|
+
self._boot()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
vars(self)["fault"] = str(e)
|
|
152
|
+
printf(f"{time.strftime('%H:%M:%S')} {e}", Sty.GRAY)
|
|
153
|
+
raise
|
|
154
|
+
vars(self)["fault"] = None
|
|
155
|
+
|
|
156
|
+
def _boot(self):
|
|
157
|
+
self._poe(True)
|
|
158
|
+
started = time.monotonic()
|
|
159
|
+
link_checked = False
|
|
160
|
+
while not _iiod_up(self._ip):
|
|
161
|
+
waited = time.monotonic() - started
|
|
162
|
+
if waited > BOOT_TIMEOUT_S:
|
|
139
163
|
self._poe(False)
|
|
140
|
-
raise RuntimeError(f"{self.uri}
|
|
141
|
-
|
|
164
|
+
raise RuntimeError(f"PoE Pluto {self.uri} did not boot within {BOOT_TIMEOUT_S} s on {self._port}")
|
|
165
|
+
if waited > LINK_CHECK_S and not link_checked:
|
|
166
|
+
link_checked = True
|
|
167
|
+
try:
|
|
168
|
+
running = port_running(port=self._port, **self._switch)
|
|
169
|
+
except Exception:
|
|
170
|
+
running = True # switch unreachable: keep waiting for IIOD
|
|
171
|
+
if not running:
|
|
172
|
+
self._poe(False)
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
f"Pluto {self.uri}: PoE on {self._port} but no Ethernet link after {LINK_CHECK_S} s (cable/port/board fault)"
|
|
175
|
+
)
|
|
176
|
+
time.sleep(1)
|
|
177
|
+
import adi
|
|
178
|
+
|
|
179
|
+
pluto = adi.Pluto(self.uri)
|
|
180
|
+
found = str(pluto._ctx.attrs.get("hw_serial", "")).strip()
|
|
181
|
+
if self._serial and found.lower() != self._serial.lower():
|
|
182
|
+
del pluto
|
|
183
|
+
self._poe(False)
|
|
184
|
+
raise RuntimeError(f"{self.uri} on {self._port} is Pluto {found}, expected {self._serial}")
|
|
185
|
+
vars(self)["_pluto"] = pluto
|
|
142
186
|
|
|
143
187
|
def power_down(self):
|
|
188
|
+
"""Cut power. The Pluto boots with default settings: sample rate, LOs,
|
|
189
|
+
gains and rx_buffer_size are all reset by the next power_up()."""
|
|
144
190
|
with self._lock:
|
|
145
191
|
# Drop the handle while the Pluto can still answer its buffer teardown.
|
|
146
192
|
vars(self)["_pluto"] = None
|
|
@@ -106,7 +106,7 @@ def _table() -> list[Command]:
|
|
|
106
106
|
# ---- devices ----
|
|
107
107
|
Command(("devices list",), "Configured devices", _shell("cmd_devices_list"), "Devices"),
|
|
108
108
|
Command(("devices status",), "Live device online/last_seen/route", _daemon("cmd_devices_status"), "Devices", where=D),
|
|
109
|
-
Command(("devices add",), "Add a device (asks type, name, serial)", _shell("cmd_devices_add"), "Devices", after="reload_devices"),
|
|
109
|
+
Command(("devices add",), "Add a device (asks type, name, serial)", _shell("cmd_devices_add"), "Devices", after="reload_devices", hidden=True),
|
|
110
110
|
Command(("devices remove",), "Remove a device by id", _shell("cmd_devices_remove"), "Devices", after="reload_devices"),
|
|
111
111
|
Command(("devices rename",), "Rename a device: devices rename <id> <name>", _shell("cmd_devices_rename"), "Devices", after="reload_devices"),
|
|
112
112
|
Command(("devices wipe",), "Remove ALL devices [-y]", _shell("cmd_devices_wipe"), "Devices", after="reload_devices"),
|
|
@@ -264,10 +264,6 @@ def cmd_help(argv: list[str]) -> int:
|
|
|
264
264
|
return 2
|
|
265
265
|
rows = [c for c in _visible(_IN_DAEMON) if c.section == section]
|
|
266
266
|
lines = [_SECTION_BLURB.get(section, ""), ""] + _columns([(prefix + c.names[0], c.help) for c in rows])
|
|
267
|
-
if section == "Devices":
|
|
268
|
-
lines += ["", "Types for `devices add`:",
|
|
269
|
-
" pluto, pluto-mimo, rtl-sdr, hackrf, ti-mmwave,",
|
|
270
|
-
" usrp-2901, usrp-b205-mini, usrp-n210, usrp-profile"]
|
|
271
267
|
print_panel(section, lines, literal=True)
|
|
272
268
|
return 0
|
|
273
269
|
lines: list[str] = []
|
{remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/device_manager.py
RENAMED
|
@@ -77,6 +77,10 @@ def _is_connected(st: _DeviceState) -> bool:
|
|
|
77
77
|
# host
|
|
78
78
|
return bool(st.online)
|
|
79
79
|
|
|
80
|
+
def _poe_fault(st: _DeviceState):
|
|
81
|
+
"""Last boot failure of a local PoE Pluto (None otherwise; see drivers/adalm_pluto/poe.py)."""
|
|
82
|
+
return getattr(getattr(st.dev, "device", None), "fault", None)
|
|
83
|
+
|
|
80
84
|
# Host device pulling/grabbing
|
|
81
85
|
|
|
82
86
|
def _get_host_registry():
|
|
@@ -187,6 +191,12 @@ def _local_devices_str_snapshot() -> Dict[int, str]:
|
|
|
187
191
|
name = (st.name or f"device-{gid}").strip()
|
|
188
192
|
|
|
189
193
|
status = "online" if _is_connected(st) else "offline"
|
|
194
|
+
fault = _poe_fault(st)
|
|
195
|
+
power = getattr(getattr(st.dev, "device", None), "power_state", None)
|
|
196
|
+
if fault:
|
|
197
|
+
status = f"offline: {fault}"
|
|
198
|
+
elif power:
|
|
199
|
+
status = f"{status}, {power}"
|
|
190
200
|
if st.salt or st.hsh:
|
|
191
201
|
status = f"{status}, reserved"
|
|
192
202
|
|
|
@@ -728,7 +738,7 @@ def describe_devices(ids) -> list[dict]:
|
|
|
728
738
|
if st is None:
|
|
729
739
|
continue
|
|
730
740
|
out.append({"id": int(gid), "name": (st.name or f"device-{gid}").strip(),
|
|
731
|
-
"type": st.dtype or "", "online": _is_connected(st)})
|
|
741
|
+
"type": st.dtype or "", "online": _is_connected(st) and not _poe_fault(st)})
|
|
732
742
|
return out
|
|
733
743
|
|
|
734
744
|
|
|
@@ -1187,7 +1197,7 @@ def device_inventory_snapshot() -> Dict[int, Dict[str, object]]:
|
|
|
1187
1197
|
gid: {
|
|
1188
1198
|
"name": st.name or f"device {gid}",
|
|
1189
1199
|
"device_type": st.dtype or None,
|
|
1190
|
-
"online": _is_connected(st),
|
|
1200
|
+
"online": _is_connected(st) and not _poe_fault(st),
|
|
1191
1201
|
"origin": st.origin,
|
|
1192
1202
|
}
|
|
1193
1203
|
for gid, st in _devices.items()
|
|
@@ -45,6 +45,7 @@ import os
|
|
|
45
45
|
import secrets
|
|
46
46
|
import socket
|
|
47
47
|
import threading
|
|
48
|
+
import time
|
|
48
49
|
from pathlib import Path
|
|
49
50
|
from typing import Optional
|
|
50
51
|
|
|
@@ -125,6 +126,20 @@ ALPN = "remoterf-direct/1"
|
|
|
125
126
|
MAX_FRAME = 100 * 1024 * 1024 # the gRPC message ceiling
|
|
126
127
|
IDLE_SECONDS = 600
|
|
127
128
|
CONNECT_SECONDS = 30 # ICE checks, then the QUIC handshake; a silent client is dropped
|
|
129
|
+
# How long the punch waits for the client's first check when the answer also
|
|
130
|
+
# offered the fixed port. A 2.1.0 client dials that port for 2 s before its
|
|
131
|
+
# first check; a check of ours reaching its NAT before then leaves a stale
|
|
132
|
+
# entry there that shadows the port its own checks are then mapped to (Linux
|
|
133
|
+
# NAT picks another), and the punch can never complete. A check that arrives
|
|
134
|
+
# sooner ends the wait; a client that never reaches us gets ours after it.
|
|
135
|
+
# 3.0 = the 2 s dial plus ~1 s for the relay round trip that carries the
|
|
136
|
+
# answer; a shorter hold reproduces the shadow on a slow relay.
|
|
137
|
+
HOLD_SECONDS = 3.0
|
|
138
|
+
# A client that punches at once ("race" in its offer) sends its first check
|
|
139
|
+
# as soon as the answer reaches it, but that check is dropped at a border
|
|
140
|
+
# that has not seen ours yet, so it cannot end the hold: ours must simply
|
|
141
|
+
# leave after its first check has, and before its retransmit at 0.5 s.
|
|
142
|
+
RACE_HOLD_SECONDS = 0.4
|
|
128
143
|
MAX_PATHS_PER_SESSION = 8 # processes of one login; the oldest path makes room
|
|
129
144
|
DEFAULT_STUN = "stun.l.google.com:19302"
|
|
130
145
|
STUN_SECONDS = 1.5 # a STUN server answers in milliseconds or not at all
|
|
@@ -145,6 +160,23 @@ _listener = None
|
|
|
145
160
|
_listener_port = 0
|
|
146
161
|
|
|
147
162
|
|
|
163
|
+
def say(message: str) -> None:
|
|
164
|
+
"""A console line about a path, stamped: paths fail minutes apart."""
|
|
165
|
+
printf(f"{time.strftime('%H:%M:%S')} {message}", Sty.GRAY)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def describe_failure(exc: BaseException, stage: str) -> str:
|
|
169
|
+
"""The exception class and a message that says what did not happen."""
|
|
170
|
+
if isinstance(exc, asyncio.TimeoutError):
|
|
171
|
+
return f"TimeoutError: {stage} did not complete within {CONNECT_SECONDS} s"
|
|
172
|
+
return f"{type(exc).__name__}: {exc}"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def describe_remotes(ice: Connection) -> str:
|
|
176
|
+
"""The candidates the client offered, host:port/type each."""
|
|
177
|
+
return " ".join(f"{c.host}:{c.port}/{c.type}" for c in ice.remote_candidates) or "none"
|
|
178
|
+
|
|
179
|
+
|
|
148
180
|
def frame(payload: bytes) -> bytes:
|
|
149
181
|
return len(payload).to_bytes(4, "big") + payload
|
|
150
182
|
|
|
@@ -187,8 +219,15 @@ class Ice(Connection):
|
|
|
187
219
|
server, and aioice would hold gathering for its whole 5 s timeout on each
|
|
188
220
|
one, on every offer. One interface and a short STUN wait instead; a
|
|
189
221
|
machine without a default route gathers the way aioice does.
|
|
222
|
+
|
|
223
|
+
``heard`` is set by the peer's first check, which may arrive before
|
|
224
|
+
connect() (aioice holds it as an early check until then).
|
|
190
225
|
"""
|
|
191
226
|
|
|
227
|
+
def __init__(self, *args, **kwargs):
|
|
228
|
+
super().__init__(*args, **kwargs)
|
|
229
|
+
self.heard = asyncio.Event()
|
|
230
|
+
|
|
192
231
|
async def gather_candidates(self) -> None:
|
|
193
232
|
address = default_route_address()
|
|
194
233
|
if address is None or self._local_candidates_start:
|
|
@@ -199,6 +238,10 @@ class Ice(Connection):
|
|
|
199
238
|
)
|
|
200
239
|
self._local_candidates_end = True
|
|
201
240
|
|
|
241
|
+
def request_received(self, *args) -> None:
|
|
242
|
+
super().request_received(*args)
|
|
243
|
+
self.heard.set()
|
|
244
|
+
|
|
202
245
|
|
|
203
246
|
def peer_address(ice: Connection) -> tuple[str, int]:
|
|
204
247
|
"""The remote end of the nominated pair (aioice keeps it private)."""
|
|
@@ -299,11 +342,12 @@ class _Protocol(QuicConnectionProtocol):
|
|
|
299
342
|
class _Path:
|
|
300
343
|
"""One client session's punched pair and the QUIC connection over it."""
|
|
301
344
|
|
|
302
|
-
def __init__(self, key: str, user: str, ice: Connection, bound: Optional[dict] = None):
|
|
345
|
+
def __init__(self, key: str, user: str, ice: Connection, bound: Optional[dict] = None, hold: float = 0.0):
|
|
303
346
|
self.key = key
|
|
304
347
|
self.user = user
|
|
305
348
|
self.ice = ice
|
|
306
349
|
self.bound = bound
|
|
350
|
+
self.hold = hold # seconds to wait for the client's first check (a fixed port was offered too)
|
|
307
351
|
self.peer: Optional[tuple[str, int]] = None
|
|
308
352
|
self.calls = 0
|
|
309
353
|
self.task: Optional[asyncio.Task] = None
|
|
@@ -312,24 +356,31 @@ class _Path:
|
|
|
312
356
|
|
|
313
357
|
async def run(self) -> None:
|
|
314
358
|
protocol = None
|
|
359
|
+
stage = "ICE"
|
|
315
360
|
try:
|
|
361
|
+
if self.hold:
|
|
362
|
+
with contextlib.suppress(asyncio.TimeoutError):
|
|
363
|
+
await asyncio.wait_for(self.ice.heard.wait(), self.hold)
|
|
316
364
|
await asyncio.wait_for(self.ice.connect(), CONNECT_SECONDS)
|
|
317
365
|
self.peer = peer_address(self.ice)
|
|
366
|
+
stage = "the QUIC Initial"
|
|
318
367
|
protocol = await asyncio.wait_for(self._accept(), CONNECT_SECONDS)
|
|
319
368
|
pump = asyncio.ensure_future(self._pump(protocol))
|
|
320
369
|
closed = asyncio.ensure_future(protocol.wait_closed())
|
|
321
370
|
try:
|
|
371
|
+
stage = "the QUIC handshake"
|
|
322
372
|
try:
|
|
323
373
|
await asyncio.wait_for(protocol.wait_connected(), CONNECT_SECONDS)
|
|
324
374
|
except ConnectionError:
|
|
325
375
|
raise ConnectionError(f"QUIC handshake failed: {protocol.terminated}") from None
|
|
326
376
|
where = "offsite" if is_offsite(self.peer[0]) else "local"
|
|
327
|
-
|
|
377
|
+
say(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where})")
|
|
328
378
|
await asyncio.wait({pump, closed}, return_when=asyncio.FIRST_COMPLETED)
|
|
329
379
|
finally:
|
|
330
380
|
await cancel_tasks((pump, closed))
|
|
331
381
|
except Exception as exc: # noqa: BLE001 - a failed punch is expected on some networks
|
|
332
|
-
|
|
382
|
+
held = f"; held {self.hold:g} s" if self.hold else ""
|
|
383
|
+
say(f"Direct path failed: {self.user}: {describe_failure(exc, stage)}; offered {describe_remotes(self.ice)}{held}")
|
|
333
384
|
finally:
|
|
334
385
|
if protocol is not None:
|
|
335
386
|
protocol.close()
|
|
@@ -339,7 +390,7 @@ class _Path:
|
|
|
339
390
|
await self.close()
|
|
340
391
|
if self.peer is not None:
|
|
341
392
|
why = f" ({protocol.terminated})" if protocol is not None and protocol.terminated else ""
|
|
342
|
-
|
|
393
|
+
say(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}")
|
|
343
394
|
|
|
344
395
|
async def _accept(self) -> QuicConnectionProtocol:
|
|
345
396
|
"""The client's Initial packet decides the connection, as aioquic's server does."""
|
|
@@ -423,10 +474,10 @@ async def _serve_call(path, reader: asyncio.StreamReader, writer: asyncio.Stream
|
|
|
423
474
|
if bad_frame is not None:
|
|
424
475
|
# A client that cannot frame a request is not ours; its bytes are
|
|
425
476
|
# still arriving and would only pile up in this stream's buffer.
|
|
426
|
-
|
|
477
|
+
say(f"Direct path dropped: {path.user} sent a {bad_frame}")
|
|
427
478
|
await path.drop()
|
|
428
479
|
if refused is not None:
|
|
429
|
-
|
|
480
|
+
say(f"Direct path refused: {path.user}: {refused}")
|
|
430
481
|
await path.drop()
|
|
431
482
|
|
|
432
483
|
|
|
@@ -475,7 +526,7 @@ class _Conn(_Protocol):
|
|
|
475
526
|
if self.key is not None and _paths.get(self.key) is self:
|
|
476
527
|
del _paths[self.key]
|
|
477
528
|
why = f" ({self.terminated})" if self.terminated else ""
|
|
478
|
-
|
|
529
|
+
say(f"Direct path closed: {self.user} from {self.peer[0]}:{self.peer[1]} after {self.calls} calls{why}")
|
|
479
530
|
|
|
480
531
|
def _stream(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
481
532
|
task = asyncio.ensure_future(self._hello(reader, writer) if self.key is None else _serve_call(self, reader, writer))
|
|
@@ -487,7 +538,7 @@ class _Conn(_Protocol):
|
|
|
487
538
|
await self._claim(reader, writer)
|
|
488
539
|
except Exception as exc: # noqa: BLE001 - a hello that cannot be honoured closes the connection
|
|
489
540
|
logger.warning("direct path fixed-port hello failed: %r", exc)
|
|
490
|
-
|
|
541
|
+
say(f"Direct path fixed-port hello failed: {type(exc).__name__}: {exc}")
|
|
491
542
|
self.close()
|
|
492
543
|
|
|
493
544
|
async def _claim(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
@@ -509,7 +560,7 @@ class _Conn(_Protocol):
|
|
|
509
560
|
writer.write(frame(json.dumps({"ok": True}).encode()))
|
|
510
561
|
writer.write_eof()
|
|
511
562
|
where = "offsite" if is_offsite(self.peer[0]) else "local"
|
|
512
|
-
|
|
563
|
+
say(f"Direct path open: {self.user} from {self.peer[0]}:{self.peer[1]} ({where}, fixed port)")
|
|
513
564
|
|
|
514
565
|
async def drop(self) -> None:
|
|
515
566
|
if self.key is not None and _paths.get(self.key) is self:
|
|
@@ -569,13 +620,17 @@ async def _offer(key: str, user: str, remote: dict, bound: Optional[dict] = None
|
|
|
569
620
|
except Exception:
|
|
570
621
|
await close_ice(ice)
|
|
571
622
|
raise
|
|
572
|
-
|
|
623
|
+
fixed = _fixed_port_offer(key, user, bound)
|
|
624
|
+
# A client that races the fixed port against the punch says so ("race")
|
|
625
|
+
# and needs only a short hold; a 2.1.0 offer has no such key.
|
|
626
|
+
hold = 0.0 if not fixed else RACE_HOLD_SECONDS if remote.get("race") else HOLD_SECONDS
|
|
627
|
+
path = _paths[key] = _Path(key, user, ice, bound, hold=hold)
|
|
573
628
|
path.task = asyncio.ensure_future(path.run())
|
|
574
629
|
return {
|
|
575
630
|
"ufrag": ice.local_username,
|
|
576
631
|
"pwd": ice.local_password,
|
|
577
632
|
"candidates": [candidate.to_sdp() for candidate in ice.local_candidates],
|
|
578
|
-
**
|
|
633
|
+
**fixed,
|
|
579
634
|
}
|
|
580
635
|
|
|
581
636
|
|
|
@@ -612,8 +667,9 @@ def offer(session_key: str, user: str, ice: str) -> dict:
|
|
|
612
667
|
answer = asyncio.run_coroutine_threadsafe(
|
|
613
668
|
_offer(key, user, remote), _get_loop()
|
|
614
669
|
).result(20)
|
|
615
|
-
except Exception as exc: # noqa: BLE001
|
|
616
|
-
|
|
670
|
+
except Exception as exc: # noqa: BLE001 - the console gets the detail, the client a fixed line
|
|
671
|
+
say(f"Direct path offer failed: {user}: {type(exc).__name__}: {exc}")
|
|
672
|
+
return {"ace": map_arg("direct path unavailable")}
|
|
617
673
|
return {"ice": map_arg(json.dumps(answer)), "server_name": map_arg(hostname() or "")}
|
|
618
674
|
|
|
619
675
|
|
{remoterf_server-1.1.1 → remoterf_server-1.1.2}/src/remoteRF_server/server/global_onboarding.py
RENAMED
|
@@ -24,9 +24,8 @@ State lives in ``<config dir>/global.env``:
|
|
|
24
24
|
GLOBAL_CONSENT yes | no answered once at first run
|
|
25
25
|
GLOBAL_NAME requested slug
|
|
26
26
|
GLOBAL_EMAIL contact address for a human admin
|
|
27
|
-
GLOBAL_SHARE_LAN yes | no
|
|
28
27
|
GLOBAL_STATE unregistered | requested | active | rejected
|
|
29
|
-
| suspended | left | expired
|
|
28
|
+
| suspended | left | expired | unknown
|
|
30
29
|
"""
|
|
31
30
|
|
|
32
31
|
from __future__ import annotations
|
|
@@ -316,15 +315,11 @@ def configure() -> bool:
|
|
|
316
315
|
return False
|
|
317
316
|
display_name = _prompt_display_name(name)
|
|
318
317
|
email = _prompt_email()
|
|
319
|
-
share = _confirm("Share LAN address?")
|
|
320
|
-
if share is None:
|
|
321
|
-
return False
|
|
322
318
|
update(
|
|
323
319
|
GLOBAL_CONSENT="yes",
|
|
324
320
|
GLOBAL_NAME=name,
|
|
325
321
|
GLOBAL_DISPLAY_NAME=display_name,
|
|
326
322
|
GLOBAL_EMAIL=email,
|
|
327
|
-
GLOBAL_SHARE_LAN="yes" if share else "no",
|
|
328
323
|
GLOBAL_STATE="unregistered",
|
|
329
324
|
)
|
|
330
325
|
ensure_identity()
|
|
@@ -530,13 +525,6 @@ def lan_endpoint() -> Optional[str]:
|
|
|
530
525
|
return f"{host}:{port}" if host else None
|
|
531
526
|
|
|
532
527
|
|
|
533
|
-
def lan_address() -> Optional[str]:
|
|
534
|
-
"""host:port a client on the LAN should dial, when the operator shares it."""
|
|
535
|
-
if load().get("GLOBAL_SHARE_LAN", "yes").lower() != "yes":
|
|
536
|
-
return None
|
|
537
|
-
return lan_endpoint()
|
|
538
|
-
|
|
539
|
-
|
|
540
528
|
# -----------------------------
|
|
541
529
|
# Where a call came from
|
|
542
530
|
# -----------------------------
|
|
@@ -605,7 +593,7 @@ def submit_request() -> dict:
|
|
|
605
593
|
"email": kv["GLOBAL_EMAIL"],
|
|
606
594
|
"public_key": _public_key_b64(),
|
|
607
595
|
"ssh_public_key": relay_public_key(),
|
|
608
|
-
"lan_address":
|
|
596
|
+
"lan_address": lan_endpoint(),
|
|
609
597
|
"server_version": server_version(),
|
|
610
598
|
}
|
|
611
599
|
result = call("POST", REQUEST_PATH, payload)
|
|
@@ -621,7 +609,17 @@ def verify_contact(code: str) -> dict:
|
|
|
621
609
|
|
|
622
610
|
|
|
623
611
|
def fetch_me() -> dict:
|
|
624
|
-
|
|
612
|
+
try:
|
|
613
|
+
result = call("GET", ME_PATH)
|
|
614
|
+
except GlobalError as exc:
|
|
615
|
+
# Global lost (or never had) this deployment: polling again cannot
|
|
616
|
+
# fix it, only a re-join can. The identity is kept so the re-request
|
|
617
|
+
# is keyed by the same issuer UUID.
|
|
618
|
+
if exc.code == "unknown_deployment" and state() != "unknown":
|
|
619
|
+
update(GLOBAL_STATE="unknown")
|
|
620
|
+
_warn(" RemoteRF Global has no record of this deployment; "
|
|
621
|
+
"the relay stops. Run `serverrf global join` to re-register.")
|
|
622
|
+
raise
|
|
625
623
|
_apply_me(result)
|
|
626
624
|
return result
|
|
627
625
|
|
|
@@ -783,7 +781,7 @@ def status_payload() -> dict:
|
|
|
783
781
|
"SELECT COUNT(*) FROM Reservations WHERE start_time <= ? AND end_time > ?",
|
|
784
782
|
(now, now),
|
|
785
783
|
),
|
|
786
|
-
"lan_address":
|
|
784
|
+
"lan_address": lan_endpoint(),
|
|
787
785
|
"cert_sha256": cert_sha256(),
|
|
788
786
|
}
|
|
789
787
|
assert set(payload) == STATUS_FIELDS # the consent text is the contract
|
|
@@ -791,7 +789,10 @@ def status_payload() -> dict:
|
|
|
791
789
|
|
|
792
790
|
|
|
793
791
|
def send_liveness() -> None:
|
|
792
|
+
from datetime import datetime, timezone
|
|
793
|
+
|
|
794
794
|
call("POST", REPORT_PATH, {"kind": "liveness"})
|
|
795
|
+
update(GLOBAL_LAST_SEEN_AT=datetime.now(timezone.utc).isoformat())
|
|
795
796
|
|
|
796
797
|
|
|
797
798
|
def send_status() -> None:
|
|
@@ -943,10 +944,11 @@ POLL_ACTIVE_SECONDS = 3600
|
|
|
943
944
|
LIVENESS_SECONDS = 300
|
|
944
945
|
STATUS_SECONDS = 3600
|
|
945
946
|
TICK_SECONDS = 60
|
|
947
|
+
POLL_BACKOFF_MAX_SECONDS = 900
|
|
946
948
|
|
|
947
949
|
# States the agent will never leave on its own: the operator or a Global admin
|
|
948
950
|
# has to act first, so the loop stops instead of retrying forever.
|
|
949
|
-
TERMINAL_STATES = {"left", "rejected", "expired"}
|
|
951
|
+
TERMINAL_STATES = {"left", "rejected", "expired", "unknown"}
|
|
950
952
|
|
|
951
953
|
|
|
952
954
|
def _tick(last: dict, tunnel=None) -> None:
|
|
@@ -961,7 +963,15 @@ def _tick(last: dict, tunnel=None) -> None:
|
|
|
961
963
|
return
|
|
962
964
|
interval = POLL_ACTIVE_SECONDS if current == "active" else POLL_PENDING_SECONDS
|
|
963
965
|
if now - last.get("poll", -interval) >= interval:
|
|
964
|
-
|
|
966
|
+
try:
|
|
967
|
+
fetch_me()
|
|
968
|
+
except Exception:
|
|
969
|
+
# Stamp the poll so the next attempt waits out the backoff.
|
|
970
|
+
backoff = min(2 * last.get("backoff", TICK_SECONDS), POLL_BACKOFF_MAX_SECONDS)
|
|
971
|
+
last["backoff"] = backoff
|
|
972
|
+
last["poll"] = now - interval + backoff
|
|
973
|
+
raise
|
|
974
|
+
last.pop("backoff", None)
|
|
965
975
|
last["poll"] = now
|
|
966
976
|
if tunnel is not None:
|
|
967
977
|
tunnel.supervise(now)
|
|
@@ -972,7 +982,12 @@ def _tick(last: dict, tunnel=None) -> None:
|
|
|
972
982
|
("status", STATUS_SECONDS, send_status),
|
|
973
983
|
):
|
|
974
984
|
if now - last.get(key, -interval) >= interval:
|
|
975
|
-
|
|
985
|
+
try:
|
|
986
|
+
send()
|
|
987
|
+
except GlobalError as exc:
|
|
988
|
+
if exc.code == "unknown_deployment":
|
|
989
|
+
last.pop("poll", None) # confirm with /me next tick, not in an hour
|
|
990
|
+
raise
|
|
976
991
|
last[key] = now
|
|
977
992
|
_cluster_tick()
|
|
978
993
|
|
|
@@ -1016,7 +1031,10 @@ def run_agent(stop=None, tunnel=None) -> None:
|
|
|
1016
1031
|
return
|
|
1017
1032
|
_tick(last, tunnel)
|
|
1018
1033
|
except Exception as exc: # noqa: BLE001 - never take the server down
|
|
1019
|
-
logger.warning(
|
|
1034
|
+
logger.warning(
|
|
1035
|
+
"global agent tick failed: %s (%s)",
|
|
1036
|
+
exc, getattr(exc, "code", type(exc).__name__),
|
|
1037
|
+
)
|
|
1020
1038
|
if stop is not None:
|
|
1021
1039
|
if stop.wait(TICK_SECONDS):
|
|
1022
1040
|
tunnel.stop()
|
|
@@ -1088,6 +1106,7 @@ _STATE_SUMMARY = {
|
|
|
1088
1106
|
"suspended": "suspended by a RemoteRF admin",
|
|
1089
1107
|
"left": "left RemoteRF Global",
|
|
1090
1108
|
"expired": "request expired",
|
|
1109
|
+
"unknown": "RemoteRF Global has no record of this deployment -- run: serverrf global join",
|
|
1091
1110
|
}
|
|
1092
1111
|
|
|
1093
1112
|
|
|
@@ -1118,10 +1137,12 @@ def cmd_status(refresh: bool = True) -> int:
|
|
|
1118
1137
|
|
|
1119
1138
|
# Ask Global rather than reporting a stale local answer: the operator runs
|
|
1120
1139
|
# this precisely because they are waiting to hear about their request.
|
|
1121
|
-
unreachable = None
|
|
1140
|
+
unreachable = refused = None
|
|
1122
1141
|
if refresh and state() not in TERMINAL_STATES:
|
|
1123
1142
|
try:
|
|
1124
1143
|
fetch_me()
|
|
1144
|
+
except GlobalError as exc:
|
|
1145
|
+
refused = exc.code
|
|
1125
1146
|
except Exception as exc: # noqa: BLE001 - offline is not an error here
|
|
1126
1147
|
unreachable = str(exc)
|
|
1127
1148
|
kv = load()
|
|
@@ -1136,9 +1157,9 @@ def cmd_status(refresh: bool = True) -> int:
|
|
|
1136
1157
|
("display", kv.get("GLOBAL_DISPLAY_NAME", "-")),
|
|
1137
1158
|
("hostname", hostname() or "-"),
|
|
1138
1159
|
("email", kv.get("GLOBAL_EMAIL", "-")),
|
|
1139
|
-
("share LAN", kv.get("GLOBAL_SHARE_LAN", "no")),
|
|
1140
1160
|
("tunnel", tunnel_state()),
|
|
1141
1161
|
("last report", kv.get("GLOBAL_LAST_REPORT_AT", "-")),
|
|
1162
|
+
("last seen", kv.get("GLOBAL_LAST_SEEN_AT", "-")),
|
|
1142
1163
|
]
|
|
1143
1164
|
if kv.get("GLOBAL_REASON"):
|
|
1144
1165
|
fields.append(("reason", kv["GLOBAL_REASON"]))
|
|
@@ -1147,6 +1168,8 @@ def cmd_status(refresh: bool = True) -> int:
|
|
|
1147
1168
|
|
|
1148
1169
|
if current == "unverified":
|
|
1149
1170
|
rows += ["", " serverrf global verify <code>"]
|
|
1171
|
+
if refused:
|
|
1172
|
+
rows += ["", f"(refused by RemoteRF Global ({refused}))"]
|
|
1150
1173
|
if unreachable:
|
|
1151
1174
|
rows += ["", f"(could not reach RemoteRF Global just now: {unreachable})"]
|
|
1152
1175
|
|
|
@@ -1097,9 +1097,9 @@ def _cmd_status() -> None:
|
|
|
1097
1097
|
("", ""),
|
|
1098
1098
|
("global", f"{state} -- {g._STATE_SUMMARY.get(state, '')}".rstrip(" -")),
|
|
1099
1099
|
("hostname", g.hostname() or "-"),
|
|
1100
|
-
("share LAN", kv.get("GLOBAL_SHARE_LAN", "no")),
|
|
1101
1100
|
("tunnel", g.tunnel_state()),
|
|
1102
1101
|
("last report", (kv.get("GLOBAL_LAST_REPORT_AT") or "-").split(".")[0]),
|
|
1102
|
+
("last seen", (kv.get("GLOBAL_LAST_SEEN_AT") or "-").split(".")[0]),
|
|
1103
1103
|
]
|
|
1104
1104
|
else:
|
|
1105
1105
|
fields += [("", ""), ("global", "not participating (serverrf global join)")]
|