remoteRF-server 1.1.0__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.0 → remoterf_server-1.1.2}/PKG-INFO +2 -1
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/README.md +1 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/pyproject.toml +1 -1
- {remoterf_server-1.1.0/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.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/api_token.py +19 -1
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +1 -2
- {remoterf_server-1.1.0/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.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/poe.py +62 -16
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/commands.py +2 -5
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/device_manager.py +12 -2
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/direct_path.py +69 -13
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/global_onboarding.py +45 -22
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/grpc_server.py +1 -1
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/reservation.py +98 -16
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/rpc_manager.py +14 -4
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/serverrf_cli.py +36 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/PKG-INFO +2 -1
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/SOURCES.txt +4 -1
- remoterf_server-1.1.2/tests/test_account_hardening.py +134 -0
- {remoterf_server-1.1.0 → 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.0 → remoterf_server-1.1.2}/tests/test_global_accounts_migration.py +3 -1
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_global_onboarding.py +98 -30
- {remoterf_server-1.1.0 → 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.0 → remoterf_server-1.1.2}/tests/test_rpc_manager_calln.py +11 -3
- remoterf_server-1.1.2/tests/test_wipe.py +20 -0
- remoterf_server-1.1.0/tests/test_pluto_poe.py +0 -100
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/LICENSE +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/setup.cfg +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/cluster.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/devices.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/enrollment.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/migrations.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/permissions.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/principals.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_client_sessions.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_commands.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_console.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_first_run.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_panel.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_status_push.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.1.0 → remoterf_server-1.1.2}/tests/test_usrp_uhd410_hardware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoteRF-server
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: RemoteRF server-side control package
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -932,6 +932,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
|
|
|
932
932
|
### Database
|
|
933
933
|
|
|
934
934
|
* `db purge` — Remove all database entries
|
|
935
|
+
* `wipe` — Factory reset: delete the whole config dir (devices, drivers, databases, certs) and the Global identity keys; the next `serverrf -s` runs first-time setup *(server must be stopped; prompts for confirmation, `-y` skips)*
|
|
935
936
|
|
|
936
937
|
|
|
937
938
|
## Adding Hosts
|
|
@@ -901,6 +901,7 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
|
|
|
901
901
|
### Database
|
|
902
902
|
|
|
903
903
|
* `db purge` — Remove all database entries
|
|
904
|
+
* `wipe` — Factory reset: delete the whole config dir (devices, drivers, databases, certs) and the Global identity keys; the next `serverrf -s` runs first-time setup *(server must be stopped; prompts for confirmation, `-y` skips)*
|
|
904
905
|
|
|
905
906
|
|
|
906
907
|
## Adding Hosts
|
|
@@ -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.0 → 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.0 → 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"),
|
|
@@ -168,6 +168,7 @@ def _table() -> list[Command]:
|
|
|
168
168
|
Command(("db export",), "Archive DBs, devices, keys, certs to ~/Downloads [dir]", _shell("cmd_db_export"), "Database"),
|
|
169
169
|
Command(("stats purge",), "Delete the activity log (reservation stats) [-y]", _daemon("cmd_stats_purge"), "Database", where=D),
|
|
170
170
|
Command(("db purge",), "Remove all database entries", _daemon("cmd_db_purge"), "Database", where=D),
|
|
171
|
+
Command(("wipe",), "Factory reset: delete all config and data [-y]", _shell("cmd_wipe"), "Database"),
|
|
171
172
|
]
|
|
172
173
|
|
|
173
174
|
|
|
@@ -263,10 +264,6 @@ def cmd_help(argv: list[str]) -> int:
|
|
|
263
264
|
return 2
|
|
264
265
|
rows = [c for c in _visible(_IN_DAEMON) if c.section == section]
|
|
265
266
|
lines = [_SECTION_BLURB.get(section, ""), ""] + _columns([(prefix + c.names[0], c.help) for c in rows])
|
|
266
|
-
if section == "Devices":
|
|
267
|
-
lines += ["", "Types for `devices add`:",
|
|
268
|
-
" pluto, pluto-mimo, rtl-sdr, hackrf, ti-mmwave,",
|
|
269
|
-
" usrp-2901, usrp-b205-mini, usrp-n210, usrp-profile"]
|
|
270
267
|
print_panel(section, lines, literal=True)
|
|
271
268
|
return 0
|
|
272
269
|
lines: list[str] = []
|
{remoterf_server-1.1.0 → 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
|
|